건강하지 않은 테스트

불안정한 테스트

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

불안정한 테스트는 때때로 실패하지만, 충분히 재시도하면 결국 통과하는 테스트입니다.

테스트가 불안정할 수 있는 잠재적인 원인은 무엇인가요?

상태 누수

레이블: flaky-test::state leak

설명: 데이터 상태가 이전 테스트에서 누출되었습니다. 실제 원인은 아마도 여기서 불안정한 테스트가 아닐 것입니다.

재현 난이도: 보통. 일반적으로 같은 스펙 파일을 실행하여 실패하는 파일이 문제를 재현할 때까지 반복합니다.

해결책: 이전 테스트와/또는 테스트 데이터나 환경이 수정되는 장소를 수정하여 각 테스트 후에 오염되지 않은 상태로 재설정되도록 합니다.

예시:

  • 예시 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를 하드코딩하지 마세요.
  • 테스트가 순서보다는 요소에만 신경을 써야 하는 경우, 주장(assertion)을 느슨하게 하세요.
  • 결정론적 순서를 지정하여 테스트를 수정하세요.
  • 결정론적 순서를 지정하여 앱 코드를 수정하세요.

Examples:

  • 예제 1: 데이터베이스는 500개 이상의 열이 있는 테이블을 만날 때마다 재생성됩니다. 병합 요청에서는 통과할 수 있지만, 테스트 순서가 변경되면 master에서 실패할 수 있습니다.

  • 예제 2: 테스트가 존재하지 않는 ID로 레코드를 찾으려 할 경우 오류 메시지를 반환한다는 주장을 합니다. 테스트는 존재하지 않아야 할 하드코딩된 ID(예: 42)를 사용합니다. 테스트가 테스트 스위트 초기에 실행되면, 아직 충분한 레코드가 생성되지 않았기 때문에 통과할 수 있지만, 스위트에서 나중에 실행되면 실제로 ID 42를 가진 레코드가 있을 수 있어 테스트가 실패하기 시작할 수 있습니다.

  • 예제 3: ORDER BY를 지정하지 않으면 데이터베이스는 결정론적 순서를 제공하지 않으며, 테스트에서 데이터 경합이 발생할 수 있습니다.

  • 예제 4.

SQL 쿼리 과다

Label: flaky-test::too-many-sql-queries

Description: SQL 쿼리 한도에 도달하여 Gitlab::QueryLimiting::Transaction::ThresholdExceededError가 발생했습니다.

Difficulty to reproduce: 보통, 이 실패는 쿼리 캐시의 상태에 따라 달라질 수 있으며, 이는 스펙의 순서에 의해 영향을 받을 수 있습니다.

Resolution: 쿼리 수 한도 문서를 참조하세요.

무작위 입력

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를 반환하지만 페이지가 완전히 로드되지 않거나, 요소가 웹 드라이버에 의해 감지되지 못하는 경우 (예: 뷰포트를 벗어나거나 다른 요소 뒤에 렌더링된 경우).

날짜 및 시간 민감성

라벨: flaky-test::datetime-sensitive

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

재현 난이도: 일정 날짜 이후에 테스트가 일관되게 실패하는지, 특정 시간이나 날짜에만 실패하는지에 따라 쉬움에서 보통으로 다릅니다.

해결책: 시간을 고정하는 것이 일반적으로 좋은 솔루션입니다.

예시:

  • 예시 1: 시간이 지나면 깨지는 테스트.
  • 예시 2: 월의 마지막 날에 깨지는 테스트.

불안정한 인프라

라벨: flaky-test::unstable infrastructure

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

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

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

예시:

  • 예시 1: 이 시점에서 러너가 과부하 상태입니다.
  • 예시 2: 러너가 네트워킹 문제로 인해 작업이 일찍 실패합니다.

로컬에서 flaky 테스트 재현하는 방법

  1. 로컬에서 실패를 재현하기
    • CI 작업 로그에서 RSpec seed 찾기
    • 또는 while :; do bin/rspec <spec> || break; done를 루프에서 실행하여 seed 찾기
  2. bin/rspec --seed <previously found> --bisect <spec>로 스펙 실패를 이진 검색하여 예제를 줄이기
  3. 남아있는 예제를 살펴보고 상태 누수를 주의하기
    • 예: let_it_be로 생성한 레코드를 업데이트하는 것은 일반적인 문제의 원인입니다.
  4. 수정한 후 seed로 스펙을 다시 실행하기
  5. scripts/rspec_check_order_dependence를 실행하여 스펙을 무작위 순서로 실행할 수 있는지 확인하기
  6. 다시 루프에서 while :; do bin/rspec <spec> || break; done를 실행하고 (점심을 먹으세요) 더 이상 flaky하지 않은지 확인하기

격리된 테스트

master에 flaky 테스트가 있을 때:

  1. 관련 그룹 라벨과 함께 ~"failure::flaky-test" 이슈 생성하기.
  2. 첫 번째 실패 후 테스트를 격리하기.
    테스트가 적시에 수정될 수 없는 경우, 모든 개발자의 생산성에 영향을 미치므로 격리해야 합니다.

RSpec

빠른 격리

테스트를 매우 빠르게 비활성화해야 할 필요가 없는 한 (< 10min), 대신 ~pipeline::expedited 라벨 사용을 고려하세요.

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

항상 진행해 주십시오 빠른 격리 후 장기 격리 Merge Request를 열어주세요! 이는 통합된 테스트가 CI/CD 파이프라인에서 실행되어 올바르게 수정되었음을 보장하기 위함입니다 (빠른 격리 프로젝트의 컨텍스트에서 실행되지 않는 테스트).

장기 격리

테스트가 빠르게 격리되면, 장기 격리 프로세스를 진행할 수 있습니다. 이는 머지 요청을 열어 진행할 수 있습니다.

먼저, 테스트 파일에 feature_category 메타데이터가 있는지 확인하여 테스트 파일의 올바른 귀속을 보장합니다.

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

# 단일 스펙 격리
it '성공', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  expect(response).to have_gitlab_http_status(:ok)
end

# describe/context 블록 격리
describe '#flaky-method', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  [...]
end

이는 CI에서 건너뛰어질 것입니다. 기본적으로 격리된 테스트는 로컬에서 실행됩니다.

로컬 개발에서도 --tag ~quarantine으로 실행하여 건너뛸 수 있습니다:

# Bash
bin/rspec --tag ~quarantine

# ZSH
bin/rspec --tag \~quarantine

또한, 다음 사항을 확인하세요:

  1. 머지 요청에 ~"quarantine" 레이블이 있어야 합니다.
  2. MR 설명에 일반적인 용어를 통해 이슈와 머지 요청을 연결하는 언급이 있어야 합니다. 보통의 용어

공유 예제/컨텍스트를 격리해서는 안 되며, it_behaves_likeinclude_examples의 호출을 격리할 수 없다는 점을 유의하세요:

# Rubocop에 의해 플래그가 지정됩니다
shared_examples '모든 사용자를 로드할 때', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  [...]
end

# 작동하지 않음
it_behaves_like '공유 예제', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345'

# 작동하지 않음
include_examples '공유 예제', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345'

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

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

기능 카테고리에 대한 모든 격리 테스트를 찾으려면 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('오류를 발생시켜야 합니다', () => {
  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에 수집됩니다

내부 대시보드인 internal dashboard로 모니터링하기 위해서입니다.

과거 GitLab에서 겪었던 문제들

순서에 의존하는 불안정한 테스트

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

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

  1. 먼저 불안정한 테스트 이전에 실행된 사양 목록을 얻습니다. CI 작업 출력 로그의 Knapsack node specs: 아래에서 목록을 검색할 수 있습니다.

  2. 사양 목록을 파일로 저장한 후, 실행합니다:

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

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

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

배열 순서 기대

기능 테스트

Capybara 뷰포트 크기 관련 문제
카피바라 JS 드라이버 관련 문제
카피바라 기대시간 초과

정지된 스펙

스펙이 정지되거나 CI에서 타임아웃이 발생하는 경우, 로드 인터록 감지 모니터의 교착 상태 버그로 인한 것일 수 있습니다.

진단하기 위해, 다음을 사용할 수 있습니다. sigdump 루비 스레드 덤프를 인쇄합니다:

  1. 정지된 스펙을 로컬에서 실행합니다.
  2. 다음 명령어를 실행하여 루비 스레드 덤프를 발생시킵니다:

    kill -CONT <pid>
    
  3. 스레드 덤프는 /tmp/sigdump-<pid>.log 파일에 저장됩니다.

load_interlock_aware_monitor.rb가 포함된 줄이 보이면, 이는 아마도 관련이 있습니다:

/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:17:in `mon_enter'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:22:in `block in synchronize'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
/builds/gitlab-org/gitlab/vendor/ruby/3.2.0/gems/activesupport-7.0.8.4/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'

요청을 하기 전에 팩토리를 생성하여 우회한 예시를 확인하세요:

제안 사항

테스트 파일 분할

문제가 있는 테스트를 식별하고 컨텍스트를 좁히기 위해 큰 RSpec 파일을 여러 파일로 나누는 것이 도움이 될 수 있습니다.

CI에서 작업 실패 재현

CI에서 작업 실패를 재현하는 것은 테스트가 실패하는 이유를 해결하는 데 항상 도움이 됩니다. 같은 세트의 테스트 파일을 동일한 순서로 실행해야 합니다. Knapsack을 사용하여 테스트를 동시에 분산시키기 때문에 두 파이프라인 간에 파일이 다르게 분산될 수 있습니다. 다음 단계로 작업 분산을 하드코딩할 수 있습니다:

  1. 재현하려는 작업을 찾아서 해당 작업이 실행된 커밋을 식별하고, 로컬 gitlab-org/gitlab 브랜치를 동일한 커밋으로 설정하여 동일한 프로젝트를 실행하도록 합니다.
  2. 작업 로그에서 Knapsack에 의해 분산된 스펙 파일 목록을 찾습니다. Running command: bundle exec rspec를 검색하면 이 명령의 마지막 인수에 파일 이름 목록이 포함되어야 합니다. 이 목록을 복사합니다.
  3. tooling/lib/tooling/parallel_rspec_runner.rb로 이동하여 테스트 파일 분배가 발생하는 곳을 확인합니다. 이 머지 요청을 예로 삼아, 2단계에서 복사한 파일 목록을 TEST_FILES 상수에 저장하고, rspec_command 메서드를 업데이트하여 이 목록을 RSpec가 실행하도록 합니다.
  4. spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb의 테스트를 건너뛰어 파이프라인이 조기 실패하지 않게 합니다.
  5. 특정 버전에서 파이프라인을 실행하려면 병합된 결과 파이프라인을 실행하고 싶지 않습니다. 이를 위해 MR에 병합 충돌을 도입할 수 있습니다.
  6. 스펙 순서를 보존하기 위해 spec/support/rspec_order.rb 파일을 업데이트하여 원래 실패한 작업에서 보여지는 값으로 Kernel.srand를 하드코딩합니다. 여기에서와 같이 하세요. Randomized with seed 다음에 이 값이 나오는 것을 검색하여 작업 로그에서 srand 값을 찾을 수 있습니다.

자원

느린 테스트

상위 느린 테스트

우리는 rspec_profiling_stats 프로젝트에서 테스트 지속 시간에 대한 정보를 수집합니다. 데이터는 GitLab Pages를 사용하여 이 UI에서 표시됩니다.

이슈에서는 가이드 역할을 할 수 있는 테스트 지속 시간에 대한 임계치를 정의했습니다.

임계치를 초과하는 테스트에 대해서는 테스트 이슈에서 느린 발생을 자동으로 보고하여 그룹이 이를 개선할 수 있도록 합니다.

정당한 이유로 느린 테스트가 있는 경우 문제 생성을 건너뛰려면 allowed_to_be_slow: true를 추가하세요.

날짜 기능 테스트 컨트롤러 및 요청 테스트 단위 기타 방법
2023-02-15 67.42 초 44.66 초 - 76.86 초 최대한 느린 테스트 제거
2023-06-15 50.13 초 19.20 초 27.12 45.40 초 상위 100개의 느린 테스트 평균

테스트 문서로 돌아가기