Flaky tests

Flaky test가 무엇인가요?

가끔 실패하는 테스트로, 여러 번 시도하면 결국 통과하는 테스트입니다.

테스트가 flaky한 원인은 무엇입니까?

State leak

Label: flaky-test::state leak

Description: 이전 테스트에서 데이터 상태가 누출되었습니다. 실제 원인은 아마도 여기가 아닌 것입니다.

Difficulty to reproduce: 보통 중간정도. 일반적으로 동일한 spec 파일을 여러 번 실행하여 실패하는 파일을 재현합니다.

Resolution: 이전 테스트 및/또는 테스트 데이터 또는 환경이 수정되는 위치를 수정하여 각 테스트 후에 원래의 테스트로 초기화될 수 있도록 합니다.

Examples:

  • 예시 1: let_it_be로 생성된 데이터 레코드는 테스트 예제 사이에서 공유되는데, 일부 테스트가 모델을 의도적이든 의도치 않게 수정하여 테스트 예제 사이에 동기화되지 않은 데이터를 유발할 수 있습니다. 이로 인해 후속 테스트 예제에서 PG::QueryCanceled: ERROR가 발생할 수 있습니다. 상태 누출 및 해결 옵션에 대한 자세한 내용은 GitLab 테스트 최적의 방법을 참조하세요.
  • 예시 2: 마이그레이션 테스트가 데이터베이스를 롤백하고 테스트를 수행한 후 일관되지 않은 상태로 데이터베이스를 롤업할 수 있으므로, 다음 테스트에서 특정 칼럼에 대해 알지 못할 수 있습니다.
  • 예시 3: 테스트가 다음 테스트에서 사용되는 데이터를 수정합니다.
  • 예시 4: 데이터베이스 쿼리를 위한 테스트는 깨끗한 데이터베이스에서 통과하지만, 이전 테스트 시퀀스를 처리하는 데 데이터베이스를 사용하는 CI/CD 파이프라인에서는 테스트가 실패합니다. 이는 쿼리 자체를 업데이트하여 깨끗하지 않은 데이터베이스에서 작동하도록 업데이트해야 합니다.
  • 예시 5: 비관련 데이터베이스 연결이 비동기 요청에서 다시 확인되어 테스트가 이러한 비관련 데이터베이스 연결을 실수로 사용합니다. 문제는 이 merge request에서 해결되었습니다.
  • 예시 6: 데이터베이스 연결의 최대 생존 시간은 이러한 연결이 연결을 끊도록 유도하므로, 결과적으로 이러한 연결을 기반으로 하는 트랜잭션에 의존하는 테스트가 실패하게 됩니다. 문제는 이 merge request에서 해결되었습니다.
  • 예시 7: 테스트에서 사용된 TCP 소켓이 다음 테스트 이전에 닫히지 않았으며, 동일한 포트로 다른 TCP 소켓을 사용합니다.

Dataset-specific

Label: flaky-test::dataset-specific

Description: 테스트는 데이터셋이 특정한(보통 제한적인) 상태나 순서에 있다고 가정합니다. 테스트 스위트 중에 테스트가 실행되는 시기에 따라 이것이 사실일 수도 있고 아닐 수도 있습니다.

Difficulty to reproduce: 중간정도, 이슈를 재현하려면 필요한 데이터 양이 로컬에서 어려울 수 있습니다. 순서 문제는 테스트를 여러 번 실행하여 쉽게 재현할 수 있습니다.

Resolution:

  • 데이터셋이 특정 상태인 것으로 가정하지 않도록 테스트를 수정하고 ID를 하드코딩하지 않습니다.
  • 테스트가 순서에 관심을 두는 것이 아닌 요소에만 관심이 있는 경우 단언을 완화합니다.
  • 결정적인 순서를 지정하여 테스트를 수정합니다.
  • 응용 프로그램 코드를 결정적인 순서로 지정하여 테스트를 수정합니다.

Examples:

  • 예시 1: 테이블이 500개 이상의 열을 갖는 경우 데이터베이스가 다시 생성됩니다. 테스트의 순서가 변경되면 Merge Request에서 통과할 수 있지만 나중에 master에서 실패할 수 있습니다.
  • 예시 2: 특정 ID로 레코드를 찾으려고 하는 테스트가 오류 메시지를 반환한다고 주장합니다. 테스트는 실제로 존재하지 않아야 하는 하드코딩된 ID(예: 42)를 사용합니다. 테스트가 테스트 스위트에서 일찍 실행되면 충분한 레코드가 만들어지지 않아 통과할 수 있지만, 스위트가 실행되면 ID가 실제로 42인 레코드가 있을 수 있으므로 테스트가 실패할 수 있습니다.
  • 예시 3: ORDER BY를 지정하지 않으면 데이터베이스는 결정적인 순서가 없거나 테스트에서 데이터 경주가 발생할 수 있습니다.
  • 예시 4.

Random input

Label: flaky-test::random input

Description: 테스트가 때때로 기대와 맞지 않는 무작위 값들을 사용합니다.

Difficulty to reproduce: 테스트를 로컬에서 수정하여 테스트가 실패한 시점에 사용된 “무작위 값”을 사용하여 문제를 재현하기 쉽습니다.

Resolution: 문제가 재현되면 테스트 또는 응용 프로그램을 쉽게 디버깅하고 수정할 수 있어야 합니다.

Examples:

  • 예시 1: 데이터 입력이 무작위이기 때문에 가끔 나타나는 특정 데이터를 처리하기에는 테스트가 충분히 견고하지 않습니다.

Unreliable DOM Selector

Label: flaky-test::unreliable dom selector

Description: 테스트에서 사용된 DOM 선택기가 믿을 수 없습니다.

Difficulty to reproduce: 중간에서 어려움. DOM 선택기가 중복되거나 지연된 후에 나타나는 경우에 따라 다릅니다. API나 컨트롤러에서 지연을 추가하여 문제를 재현하는 것이 도움이 될 수 있습니다.

Resolution: 이 문제에 따라 상황이 달라집니다. 요청이 완료되기를 기다린다든가, 페이지를 아래로 스크롤한다든가 할 수 있습니다.

Examples:

  • 예시 1: 하나 이상의 요소와 일치하는 고유하지 않은 CSS 선택기 또는 렌더링 시간을 허용하지 않는 비대기적 선택기 방법.
  • 예시 2: CSS 선택기는 GraphQL 요청이 완료된 후에 나타나며 UI가 업데이트됩니다.
  • 예시 3: 거짓 긍정 테스트, Capybara는 페이지 방문 후 즉시 true를 반환하고 페이지가 완전히로드되지 않았거나 요소가 webdriver에 의해 감지되지 않을 경우(예: 뷰포트 외부에 렌더링되거나 다른 요소 뒤에 렌더링된 경우).

Datetime-sensitive

레이블: flaky-test::datetime-sensitive

설명: 테스트는 특정한 날짜나 시간을 가정하고 있습니다.

재현의 난이도: 테스트가 특정 날짜 이후에 일관적으로 실패하는지, 아니면 특정 시간이나 날짜에만 실패하는지에 따라 쉽거나 적당히 어려울 수 있습니다.

해결 방법: 시간을 고정하는 것이 보통 좋은 해결책입니다.

예시:

  • 예시 1: 시간이 지난 후에 실패하는 테스트.
  • 예시 2: 한 달의 마지막 날에 실패하는 테스트.

Unstable infrastructure

레이블: flaky-test::unstable infrastructure

설명: 시스템 인프라 문제로 시간당 테스트가 가끔 실패합니다.

재현의 난이도: 어려움. CI 인프라 문제를 재현하는 것은 정말로 어렵습니다. 로컬에서 컨테이너를 사용하여 가능할 수 있습니다.

해결 방법: 해당 문제에 대한 인프라팀과의 대화를 시작하는 것이 보통 좋은 아이디어입니다.

예시:

  • 예시 1: 러너가 현재 과부하 상태입니다.
  • 예시 2: 러너의 네트워크에 문제가 있어 작업이 일찍 실패합니다.

격리된 테스트

master 브랜치에서 테스트가 불안정한 경우:

  1. 관련 그룹 레이블과 함께 ~"failure::flaky-test" 이슈를 생성합니다.
  2. 첫 번째 실패 이후에 테스트를 격리합니다. 시간적으로 빠르게 수정할 수 없는 경우, 모든 개발자의 프로덕션성에 영향을 미치므로 격리해야 합니다.

RSpec

빠른 격리

파이프라인을 열고 기다리지 않고 테스트를 빠르게 격리하려면 빠른 격리 프로세스를 따를 수 있습니다.

장기 격리

테스트가 빠르게 격리된 후, 장기 격리 프로세스를 진행할 수 있습니다. 먼저, 올바른 테스트 파일의 feature_category 메타데이터를 보장합니다.

그런 다음, 이전에 생성한 ~"failure::flaky-test" 이슈의 URL을 사용하여 quarantine: '<issue url>' 메타데이터를 추가할 수 있습니다.

it '성공함', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  expect(response).to have_gitlab_http_status(:ok)
end

이로써 CI에서 테스트가 스킵됩니다. 기본적으로 격리된 테스트는 로컬에서 실행됩니다.

로컬 개발 시에도 --tag ~quarantine과 함께 실행하여 격리된 테스트를 스킵할 수 있습니다.

bin/rspec --tag ~quarantine

장기 격리 MR이 프로덕션에 도달하면, 이전에 생성한 빠른 격리 MR을 되돌려야 합니다.

기능 카테고리별로 격리된 테스트 찾기

특정 기능 카테고리의 모든 격리된 테스트를 찾으려면 ripgrep를 사용하세요:

rg -l --multiline -w "(?s)feature_category:\s+:global_search.+quarantine:"

Jest

Jest 스펙의 경우, it.skip 메서드와 eslint-disable-next-line 주석을 사용하여 jest/no-disabled-tests ESLint 규칙을 비활성화하고 이슈 URL을 포함할 수 있습니다. 다음은 예시입니다:

// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/56789
// eslint-disable-next-line jest/no-disabled-tests
it.skip('에러를 throw해야 합니다', () => {
  expect(response).toThrowError(expected_error)
});

이는 --runInBand Jest 커맨드 라인 옵션으로 실행할 때에만 테스트가 스킵됨을 의미합니다:

jest --runInBand

격리된 스펙이 포함된 파일의 디렉터리은 다음 명령으로 찾을 수 있습니다:

yarn jest:quarantine

두 테스트 프레임워크 모두에게 이슈에 ~"quarantined test" 레이블을 추가해야 합니다.

테스트가 격리되면 다음 세 가지 선택사항이 있습니다:

  • 테스트 수정(즉, 불안정성을 해결합니다).
  • 테스트를 낮은 수준의 테스트로 이동합니다.
  • 테스트를 완전히 제거합니다(예를 들어, 낮은 수준의 테스트가 이미 존재하거나, 다른 동일 수준의 테스트와 중복되었거나, 너무 많은 것을 테스트하는 등).

자동 재시도 및 불안정한 테스트 감지

저희 CI에서는 RSpec::Retry를 사용하여 실패한 예를 몇 번 자동으로 다시 시도합니다 (정확한 재시도 횟수는 spec/spec_helper.rb에서 확인할 수 있습니다).

또한 사용자 정의 Gitlab::RspecFlaky::Listener를 사용합니다. 이 리스너는 master 브랜치의 maintenance 스케줄된 파이프라인의 update-tests-metadata 작업에서 실행되어 rspec/flaky/report-suite.json로 불안정한 예제를 저장합니다. 이 파일은 모든 파이프라인에서 retrieve-tests-metadata 작업에 의해 검색됩니다.

이 기능은 원래 https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13021에서 구현되었습니다.

로컬에서 재시도를 원한다면 RETRIES 환경 변수를 사용할 수 있습니다. 예를 들어 RETRIES=1 bin/rspec ...는 실패한 예를 한 번 다시 시도합니다.

로컬에서 보고서를 생성하려면 FLAKY_RSPEC_GENERATE_REPORT 환경 변수를 사용합니다. 예를 들어, FLAKY_RSPEC_GENERATE_REPORT=1 bin/rspec ...입니다.

rspec/flaky/report-suite.json 보고서의 사용법

rspec/flaky/report-suite.json 보고서는 매일 한 번 Snowflake로 import되어 internal dashboard로 모니터링됩니다.

GitLab에서 과거에 겪었던 문제점

순서에 따라 다른 결과를 보이는 불안정한 테스트

이러한 불안정한 테스트는 다른 테스트와 함께 실행되는 순서에 따라 실패할 수 있습니다. 예를 들어:

이러한 실패를 재현하기 위해 최소한의 테스트 조합을 제공하는 scripts/rspec_bisect_flaky를 사용하여 해당 실패를 유발하는 테스트를 식별할 수 있습니다:

  1. 먼저 불안정한 테스트 이전에 실행된 테스트 디렉터리을 얻어야 합니다. CI 작업 출력 로그에서 Knapsack node specs: 아래에서 디렉터리을 찾을 수 있습니다.
  2. 테스트 디렉터리을 파일로 저장한 후 다음을 실행하세요:

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

만약 순서에 의존하는 문제가 있다면, 위의 스크립트는 최소한의 재현을 출력할 것입니다.

시간에 민감한 불안정한 테스트

배열 순서 기대

기능 테스트

Capybara 뷰포트 크기 관련 문제

Capybara JS 드라이버 관련 문제

Capybara expectation times out

Hanging specs

만약 특정 스펙이 멈춘다면, 이것은 Rails의 버그에 의한 것일 수 있습니다:

제안사항

테스트 파일 분할

큰 RSpec 파일을 여러 파일로 나누어 문제가 되는 테스트를 식별하는 데 도움이 될 수 있습니다.

CI에서 동일한 테스트 파일 집합을 실행하여 작업 실패 재현

CI에서 작업 실패를 재현하는 것은 항상 테스트가 왜 실패했는지에 대한 문제 해결에 도움이 됩니다. 이를 위해 두 파이프라인 간에 파일이 다르게 분배될 수 있기 때문에 Knapsack를 사용하여 테스트를 병렬 작업으로 분할하고 파일이 다르게 분배될 수 있기 때문에 이 작업 분배를 강제로 만들어야 합니다. 다음 단계를 수행하세요:

  1. 재현하고 싶은 작업을 찾고, 해당 작업이 실행된 커밋을 식별하여 동일한 프로젝트 사본으로 실행되도록 로컬 gitlab-org/gitlab 브랜치를 동일한 커밋으로 설정합니다.
  2. 작업 로그에서 Knapsack에 의해 분배된 특정 파일 디렉터리을 찾습니다. Running command: bundle exec rspec를 검색하여 이 명령의 마지막 인자에 파일 이름 디렉터리이 포함되어 있어야 합니다. 이 디렉터리을 복사합니다.
  3. 테스트 파일 분배가 발생하는 위치인 tooling/lib/tooling/parallel_rspec_runner.rb로 이동합니다. 여기서 예시로 사용된 이 MR을 참조하여, 2단계에서 복사한 파일 디렉터리을 TEST_FILES 상수에 저장하고, 예시 MR에서 수행된 것처럼 rspec_command 메서드를 업데이트하여 RSpec이 이 디렉터리을 실행하도록 합니다.
  4. 파이프라인이 일찍 실패하지 않도록 spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb에서 테스트를 건너뜁니다.
  5. 특정 버전에서 파이프라인이 실행되도록 강제하기 위해 MR에 Merge 충돌을 도입합니다.
  6. 특별한 버전에서 테스트를 수행하기 위해 spec/support/rspec_order.rb 파일을 업데이트하여, 원래 실패한 작업에서 보여진 Kernel.srand 값을 하드코딩합니다. Randomized with seed를 검색하여 작업 로그에서 srand 값을 찾을 수 있습니다. (여기를 참조하세요.)

리소스


테스트 문서로 돌아가기