불안정한 테스트

불안정한 테스트란 무엇인가요?

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

테스트가 불안정해지는 잠재적인 원인은 무엇인가요?

상태 유출

Label: flaky-test::state leak

Description: 이전 테스트에서 데이터 상태가 유출되었습니다. 실제 원인은 아마도 여기서 불안정한 테스트의 원인은 아닙니다.

Difficulty to reproduce: 보통 중간 정도. 일반적으로 동일한 스펙 파일을 여러 번 실행하여 문제를 재현합니다.

Resolution: 이전 테스트 및/또는 테스트 데이터 또는 환경이 수정된 위치를 수정하여 각 테스트 후에 깨끗한 상태로 초기화될 수 있도록 수정하세요.

Examples:

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

데이터 집합별

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.

임의의 입력

Label: flaky-test::random input

Description: 테스트는 때로는 기대치와 일치하는 무작위 값이 사용됩니다.

Difficulty to reproduce: 쉬움. 테스트를 로컬에서 수정하여 테스트가 실패한 시점에 “임의의 값”을 사용할 수 있습니다.

Resolution: 문제가 재현되면 테스트나 앱을 쉽게 디버깅하고 수정할 수 있어야 합니다.

Examples:

  • 예시 1: 데이터 입력이 무작위로 나타나는 경우 가끔가다 기대대로 처리할 수 없는 데이터를 실패하는 테스트는 강건하지 않습니다.

신뢰할 수 없는 DOM 선택기

Label: flaky-test::unreliable dom selector

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

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

Resolution: 이 문제에 따라 다릅니다. 요청이 완료될 때까지 기다리거나 페이지를 아래로 스크롤하는 등의 방법일 수 있습니다.

Examples:

  • 예시 1: 고유하지 않은 CSS 선택기가 하나 이상의 요소와 일치하거나 렌더링 전에 시간을 줄 수 없는 선택기 방법을 사용하여 element not found 오류를 발생시킵니다.
  • 예시 2: CSS 선택기는 GraphQL 요청이 완료되고 UI가 업데이트된 후에 나타납니다.
  • 예시 3: 잘못된 양성 테스트로 Capybara는 페이지 방문 후 즉시 true를 반환하며 페이지가 완전히로드되지 않거나 요소가 webdriver에 의해 감지할 수 없으면(뷰포트 외부에 렌더링되거나 다른 요소 뒤에 렌더링되는 경우) 실패할 수 있습니다.

날짜 및 시간에 민감한

Label: flaky-test::datetime-sensitive

Description: 테스트에서 특정 날짜나 시간을 가정하고 있습니다.

Difficulty to reproduce: 쉽게 중간까지, 테스트가 일정한 날짜 이후에 항상 실패하는 경우 또는 특정 시간이나 날짜에서만 실패하는 경우에 따라 다릅니다.

Resolution: 시간을 정지시키는 것이 일반적으로 좋은 해결책입니다.

Examples:

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

불안정한 인프라

레이블: flaky-test::unstable infrastructure

설명: 테스트가 때때로 인프라 문제로 실패합니다.

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

해상: 전용 이슈에서 인프라 부서와 대화를 시작하는 것이 일반적으로 좋은 생각입니다.

예시:

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

로컬에서 불안정한 테스트를 어떻게 재현하나요?

  1. 로컬에서 실패를 재현합니다.
    • CI 작업 로그에서 RSpec seed를 찾습니다.
    • 또는 while :; do bin/rspec <spec> || break; done을(를) 반복해서 실행하여 seed를 찾습니다.
  2. bin/rspec --seed <이전에 찾은 시드> --bisect <spec>로 특정 실패를 이분법적으로 줄입니다.
  3. 남은 예시를 확인하고 상태 누출을 주의합니다.
    • 예: let_it_be로 생성된 레코드를 업데이트하는 것은 일반적인 문제의 원인입니다.
  4. 수정한 후, seed로 테스트를 다시 실행합니다.
  5. 랜덤한 순서에서 테스트를 실행할 수 있는지 확인하기 위해 scripts/rspec_check_order_dependence를 실행합니다.
  6. 다시 반복으로 while :; do bin/rspec <spec> || break; done을 실행하여(를) 점심을 먹으면서 플라키하지 않는지 확인합니다.

격리된 테스트

master에서 불안정한 테스트가 있을 때:

  1. 관련 그룹 레이블이 있는 ~"failure::flaky-test" 이슈를 생성합니다.
  2. 첫 번째 실패 후 테스트를 격리합니다. 해당 테스트를 적시에 고칠 수 없는 경우 모든 개발자의 프로덕션성에 영향을 미치므로 격리되어야 합니다.

RSpec

빠른 격리

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

장기 격리

테스트를 빠르게 격리한 후 장기 격리 프로세스를 진행할 수 있습니다. 이는 Merge Request을 열어서 수행할 수 있습니다.

먼저, 테스트 파일에 feature_category 메타데이터가 있는지 확인하여 테스트 파일이 올바르게 속성이 부여되도록 합니다.

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

it 'succeeds', 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

장기 격리 Merge Request이 프로덕션에 도달하면 이전에 만든 빠른 격리 Merge Request을 되돌려야 합니다.

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

ripgrep를 사용하여 기능 카테고리별로 모든 격리된 테스트를 찾을 수 있습니다:

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

Jest

Jest 스펙에서는 .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('should throw an error', () => {
  expect(response).toThrowError(expected_error)
});

이는 --runInBand Jest 명령줄 옵션으로 실행될 때만 건너뜁니다:

jest --runInBand

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

yarn jest:quarantine

두 테스트 프레임워크 모두 이슈에 ~"quarantined test" 레이블을 추가하세요.

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

  • 테스트를 수정합니다(즉, 플라키성을 제거합니다).
  • 테스트를 낮은 수준의 테스팅으로 이동합니다.
  • 테스트를 완전히 제거합니다(예: 이미 낮은 수준의 테스트가 있는 경우, 또는 다른 동일 수준의 테스트가 중복되어 있는 경우, 또는 너무 많은 테스트 등).

자동 재시도 및 플라키 테스트 감지

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

또한 커스텀 Gitlab::RspecFlaky::Listener를 사용합니다. 이 리스너는 maintenance 예정된 파이프라인의 update-tests-metadata 작업에서 master 브랜치에서 플라키 예제를 실행하고 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로 가져와져 내부 대시보드에서 모니터링됩니다.

GitLab에서 경험한 문제점

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

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

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

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

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

순서 의존성 문제가 있는 경우, 위의 스크립트는 최소한의 재현을 출력할 것입니다.

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

배열 순서 기대값

기능 테스트

Capybara 뷰포트 크기 관련 문제

Capybara JS 드라이버 관련 문제

Capybara 예상 시간 초과

고정된 스펙

만약 스펙이 멈추면 Rails의 버그로 인한 것일 수 있습니다:

제안

테스트 파일 분할

대형 RSpec 파일을 여러 파일로 나누면 문맥을 좁히고 문제가 되는 테스트를 식별하는 데 도움이 될 수 있습니다.

CI에서 작업 실패 재현하기

CI에서 작업 실패를 다시 재현하는 것은 언제나 테스트가 왜 실패했는지 어떻게 해야 하는지 해결하는 데 도움이 됩니다. 이를 위해서는 동일한 테스트 파일을 사용하여 작업을 다시 실행해야 합니다. 우리는 테스트를 병렬 작업을 위해 Knapsack를 사용하고 파일이 두 개의 파이프라인 사이에서 다르게 분산될 수 있기 때문에 이 작업 분산을 다음 단계를 통해 직접 지정할 수 있습니다:

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

리소스


테스트 문서로 돌아가기