건강하지 않은 테스트

불안정한 테스트

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

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

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

상태 누수

레이블: flaky-test::state leak

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

재현 난이도: 보통입니다. 일반적으로 동일한 스펙 파일을 실행하여 실패하는 스펙을 재현합니다.

해결 방법: 이전 테스트 및/또는 테스트 데이터 또는 환경이 수정된 위치를 수정하여 각 테스트 이후에 pristine 상태로 재설정되도록 합니다.

예시:

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

데이터셋별

레이블: flaky-test::dataset-specific

설명: 테스트가 데이터셋이 특정(보통 제한된) 상태 또는 순서에 있다고 가정합니다. 테스트가 테스트 스위트를 실행하는 동안에는 이러한 가정이 사실이 아닐 수 있습니다.

재현 난이도: 중간입니다. 이 문제를 재현하기 위해 필요한 데이터 양이 로컬에서 얻기 어렵습니다. 순서 문제는 테스트를 반복적으로 여러 번 실행하여 쉽게 재현할 수 있습니다.

해결 방법:

  • 데이터셋이 특정 상태에 있다고 가정하는 대신 테스트를 수정하여 ID를 하드코딩하지 않도록 합니다.
  • 테스트가 순서에 신경 쓰지 않고 요소에 대해서만 신경 써야 한다면 단언을 완화합니다.
  • 결정적인 순서를 지정하여 테스트를 수정합니다.
  • 앱 코드를 결정적인 순서로 지정하여 테스트를 수정합니다.

예시:

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

너무 많은 SQL 쿼리

레이블: flaky-test::too-many-sql-queries

설명: SQL 쿼리 한도에 도달하여 Gitlab::QueryLimiting::Transaction::ThresholdExceededError가 트리거됩니다.

재현 난이도: 중간입니다. 이 실패는 쿼리 캐시의 상태에 따라 달려있으며 이는 스펙의 순서에 영향을 받을 수 있습니다.

해결 방법: 쿼리 횟수 제한 문서를 참조하세요.

랜덤 입력

레이블: flaky-test::random input

설명: 테스트는 가끔 기대와 일치하는 랜덤 값을 사용하거나 그렇지 않을 수 있습니다.

재현 난이도: 쉽습니다. 문제가 재현되면 테스트를 로컬에서 수정하여 해당 “랜덤 값”을 사용할 수 있습니다.

해결 방법: 문제가 재현되면 테스트 또는 앱을 쉽게 디버그하고 수정할 수 있어야 합니다.

예시:

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

불안정한 DOM 선택기

Label: flaky-test::unreliable dom selector

Description: 테스트에 사용된 DOM 선택기가 불안정합니다.

Difficulty to reproduce: 중간부터 어려움. DOM 선택기가 중복되는지, 지연 후 나타나는지 등에 따라 다름. API나 컨트롤러에 지연을 추가하면 이슈를 재현하기 쉬워질 수 있습니다.

Resolution: 이 문제에 따라 다르지만, 종종 요청이 완료될 때까지 기다리거나 페이지를 아래로 스크롤하는 것이 해결책이 될 수 있습니다.

Examples:

  • Example 1: 고유하지 않은 CSS 선택기가 한 개 이상의 요소와 매치되거나, 렌더링 시간을 허용하지 않는 기다리지 않는 선택기 방법으로 인해 element not found 오류가 발생하는 경우.
  • Example 2: CSS 선택기가 GraphQL 요청이 완료된 후에 나타나며 UI가 업데이트된 경우.
  • Example 3: false-positive 테스트, Capybara는 페이지 방문 후 즉시 true를 반환하고 페이지가 완전히 로드되지 않은 경우, 또는 웹 드라이버가 감지할 수 없는 경우(뷰포트 외부에 렌더링되거나 다른 요소 뒤에 렌더링되는 경우)입니다.

날짜 및 시간에 민감한

Label: flaky-test::datetime-sensitive

Description: 테스트는 특정 날짜나 시간을 가정합니다.

Difficulty to reproduce: 쉽게 중간부터 어려움. 테스트가 특정 날짜 이후에 일관되게 실패하는지, 또는 특정 시간이나 날짜에만 실패하는지에 따라 다름.

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

Examples:

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

불안정한 인프라

Label: flaky-test::unstable infrastructure

Description: CI 인프라 문제로 테스트가 가끔 실패합니다.

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

Resolution: 전용 이슈에서 인프라 부서와 대화를 시작하는 것이 보통 좋은 아이디어입니다.

Examples:

  • Example 1: 러너가 현재 많은 부하를 받고 있습니다.
  • Example 2: 러너에 네트워크 문제가 있어 작업이 이른 시간에 실패합니다.

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

  1. 로컬에서 실패를 재현하세요
    • CI 작업 로그에서 RSpec seed를 찾으세요
    • 또는 while :; do bin/rspec <spec> || break; done을 반복해서 실행하여 seed를 찾으세요
  2. bin/rspec --seed <이전에 찾은>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

빠른 격리

테스트를 매우 빨리 비활성화해야 하는 경우(10분 미만), 대신 ~pipeline::expedited 라벨을 사용하는 것을 고려하세요.

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

빠른 격리 후 무슨 일이 일어나고 있는지 확인하기 위해 장기 격리 MR을 항상 열어주세요! 빠른 격리 프로젝트의 테스트는 빠른 격리 프로젝트의 컨텍스트에서 실행되지 않기 때문입니다.

장기 격리

테스트를 빠르게 격리한 후, 장기 격리 프로세스를 진행하실 수 있습니다. 이 과정은 MR을 열어 진행할 수 있습니다.

먼저, 올바른 테스트 파일에 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

# 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. MR에 ~"quarantine" 라벨이 있는지
  2. MR 설명에 표준 용어로 링크된 MR에 대해 언급되었는지

공유된 예제/컨텍스트를 격리해서는 안되며, it_behaves_likeinclude_examples격리할 수 없습니다:

# Rubocop에서 플래그 처리될 것입니다
shared_examples 'loads all the users when opened', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345' do
  [...]
end

# 동작하지 않음
it_behaves_like 'a shared example', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/12345'

# 동작하지 않음
include_examples 'a shared example', 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('should throw an error', () => {
  expect(response).toThrowError(expected_error)
});

이는 --runInBand Jest 명령줄 옵션으로 테스트 스위트가 실행될 때에만 스킵됩니다:

jest --runInBand

격리된 스펙이 포함된 파일 목록은 다음 명령어로 찾을 수 있습니다:

yarn jest:quarantine

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

테스트가 격리된 후에는 3가지 선택지가 있습니다:

  • 테스트 수정(즉, 방해요소를 제거하다).
  • 테스트를 더 낮은 수준의 테스트로 이동.
  • 테스트를 완전히 제거(예: 이미 더 낮은 수준의 테스트가 있거나, 다른 동일 수준의 테스트를 중복하거나, 너무 많은 것을 테스트하는 등).

자동 재시도 및 방해요소 테스트 검출

저희 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에 가져오기되어 내부 대시보드와 모니터링됩니다.

GitLab에서 겪은 문제들

순서 의존적인 방해요소 테스트

이러한 방해요소 테스트는 다른 테스트와 실행 순서에 따라 실패할 수 있습니다:

이러한 실패를 일으키는 테스트를 확인하려면 scripts/rspec_bisect_flaky를 사용하여 실패를 재현하는 최소한의 테스트 조합을 얻을 수 있습니다:

  1. 먼저 실패한 테스트 이전에 실행된 스펙 목록을 가져옵니다. CI 작업 출력 로그에서 Knapsack node specs: 아래의 목록을 찾을 수 있습니다.
  2. 스펙 목록을 파일로 저장한 다음 다음을 실행합니다:

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

순서 의존성 문제가 있는 경우 위의 스크립트에서 실패를 최소화한 재현이 출력됩니다.

시간이 중요한 불안정한 테스트

배열 순서 예상

기능 테스트

Capybara 뷰포트 크기 관련 문제
Capybara JS 드라이버 관련 문제
Capybara 기대시간 만료

매달린 사양

만약 특정 사양이 매달리거나 CI에서 시간 초과되면, 이는 Rails의 LoadInterlockAwareMonitor 데드락 버그의 결과일 수 있습니다.

진단을 위해 다음을 사용할 수 있습니다. 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에 의해 분산된 테스트 파일 목록을 찾습니다. bundle exec rspec를 검색하면, 이 명령의 마지막 인수에 파일 이름 목록이 포함되어 있어야 합니다. 이 목록을 복사합니다.
  3. 테스트 파일 분배가 발생하는 tooling/lib/tooling/parallel_rspec_runner.rb로 이동합니다. 이 MR를 참고해보세요. 단계 2에서 복사한 파일 목록을 TEST_FILES 상수에 저장하고, 예제 MR에서 수행한 것처럼 RSpec가 이 목록을 실행하도록 rspec_command 메소드를 업데이트합니다.
  4. 아래의 단계를 참조하여 spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb의 테스트를 건너뛰어서 파이프라인이 이른 시기에 실패하지 않도록 합니다.
  5. 특정 버전에 대해 파이프라인이 실행되도록 하고자 하기 때문에 병합된 결과 파이프라인을 실행하지 않고 싶을 것입니다. 이를 달성하려면 MR에 병합 충돌을 도입할 수 있습니다.
  6. 테스트 순서를 유지하기 위해, 원래 실패한 작업에서 표시된 값으로 spec/support/rspec_order.rb 파일을 업데이트합니다. 여기에서 보여진 대로 Kernel.srand 값을 하드코딩합니다. 이 값을 Randomized with seed로 검색하여 확인할 수 있습니다. 중요: 불편을 드려 정말 죄송합니다만, 저는 온라인 상태가 아니라서 언제든지 추가 도움을 드릴 수 없습니다.

자원

느린 테스트

최상위 느린 테스트

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

이슈에서는 가이드로 작용할 수 있는 테스트 소요 시간의 임계값을 정의했습니다.

임계값을 초과하는 테스트에 대해서는 해당 그룹이 개선할 수 있도록 테스트 이슈에 느림 현상을 자동으로 보고합니다.

합법적인 이유로 느린 테스트에 대해 이슈 생성을 건너뛰려면 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개 느린 테스트 평균

테스트 문서로 돌아가기