건강하지 않은 테스트
불안정한 테스트
불안정한 테스트란 무엇인가요?
불안정한 테스트는 때때로 실패하지만, 충분히 재시도하면 결국 통과하는 테스트입니다.
테스트가 불안정할 수 있는 잠재적인 원인은 무엇인가요?
상태 누수
레이블: 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
)를 사용합니다. 테스트가 테스트 스위트 초기에 실행되면, 아직 충분한 레코드가 생성되지 않았기 때문에 통과할 수 있지만, 스위트에서 나중에 실행되면 실제로 ID42
를 가진 레코드가 있을 수 있어 테스트가 실패하기 시작할 수 있습니다. -
예제 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
설명: 테스트가 특정 날짜 또는 시간을 가정하고 있습니다.
재현 난이도: 일정 날짜 이후에 테스트가 일관되게 실패하는지, 특정 시간이나 날짜에만 실패하는지에 따라 쉬움에서 보통으로 다릅니다.
해결책: 시간을 고정하는 것이 일반적으로 좋은 솔루션입니다.
예시:
불안정한 인프라
라벨: flaky-test::unstable infrastructure
설명: 인프라 문제로 인해 테스트가 가끔 실패합니다.
재현 난이도: 어려움. CI 인프라 문제는 재현하기 정말 어렵습니다. 로컬에서 컨테이너를 사용하여 가능할 수 있습니다.
해결책: 전담 이슈에서 인프라 부서와 대화를 시작하는 것이 일반적으로 좋은 생각입니다.
예시:
로컬에서 flaky 테스트 재현하는 방법
- 로컬에서 실패를 재현하기
- CI 작업 로그에서 RSpec
seed
찾기 - 또는
while :; do bin/rspec <spec> || break; done
를 루프에서 실행하여seed
찾기
- CI 작업 로그에서 RSpec
-
bin/rspec --seed <previously found> --bisect <spec>
로 스펙 실패를 이진 검색하여 예제를 줄이기 - 남아있는 예제를 살펴보고 상태 누수를 주의하기
- 예:
let_it_be
로 생성한 레코드를 업데이트하는 것은 일반적인 문제의 원인입니다.
- 예:
- 수정한 후
seed
로 스펙을 다시 실행하기 -
scripts/rspec_check_order_dependence
를 실행하여 스펙을 무작위 순서로 실행할 수 있는지 확인하기 - 다시 루프에서
while :; do bin/rspec <spec> || break; done
를 실행하고 (점심을 먹으세요) 더 이상 flaky하지 않은지 확인하기
격리된 테스트
master
에 flaky 테스트가 있을 때:
- 관련 그룹 라벨과 함께 ~"failure::flaky-test" 이슈 생성하기.
- 첫 번째 실패 후 테스트를 격리하기.
테스트가 적시에 수정될 수 없는 경우, 모든 개발자의 생산성에 영향을 미치므로 격리해야 합니다.
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
또한, 다음 사항을 확인하세요:
- 머지 요청에 ~"quarantine" 레이블이 있어야 합니다.
- MR 설명에 일반적인 용어를 통해 이슈와 머지 요청을 연결하는 언급이 있어야 합니다. 보통의 용어
공유 예제/컨텍스트를 격리해서는 안 되며, it_behaves_like
나 include_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에서 겪었던 문제들
-
rspec-retry
가 일부 API 사양 실패 시 문제를 일으킵니다: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9825 -
PG::UniqueViolation
로 인한 간헐적인 RSpec 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9846 - ffaker가 테스트가 처리할 준비가 되어 있지 않은 엉뚱한 데이터를 생성합니다(테스트는 예측 가능해야 하므로 좋지 않습니다!):
-
spec/mailers/notify_spec.rb
를 더 강력하게 만들기: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10015 -
spec/requests/api/commits_spec.rb
의 일시적인 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9944 - ffaker 팩토리 데이터를 시퀀스로 교체하기: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10184
-
spec/finders/issues_finder_spec.rb
의 일시적인 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10404
-
순서에 의존하는 불안정한 테스트
이러한 불안정한 테스트는 다른 테스트와 실행되는 순서에 따라 실패할 수 있습니다. 예를 들어:
이러한 실패를 유발하는 테스트를 식별하기 위해 scripts/rspec_bisect_flaky
를 사용할 수 있으며, 이는 실패를 재현하기 위한 최소 테스트 조합을 제공합니다:
-
먼저 불안정한 테스트 이전에 실행된 사양 목록을 얻습니다. CI 작업 출력 로그의
Knapsack node specs:
아래에서 목록을 검색할 수 있습니다. -
사양 목록을 파일로 저장한 후, 실행합니다:
cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
순서 의존성 문제가 있는 경우 위의 스크립트는 최소 재현을 출력합니다.
시간에 민감한 불안정한 테스트
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10046
- https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10306
배열 순서 기대
기능 테스트
- 테스트가 시작되기 전에 필요한 모든 데이터를 생성하는 것을 잊지 마세요: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12059
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12604
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12664
- 페이지 콘텐츠가 아닌 기본 데이터베이스 상태를 기준으로 단언합니다: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10934
- JS 테스트에서 요소를 이동시키는 것은 요소가 Capybara가 클릭을 보낼 때 정확히 이동할 경우 Capybara가 잘못 클릭하게 할 수 있습니다
- 이벤트 핸들러가 설정되기 전에 JS 이벤트를 트리거합니다
- Markdown 이미지의
src
속성을 단언할 때 이미지를 lazy-roading할 때까지 기다리기 - 플래시 공지 배너에 대한 단언을 피하세요
Capybara 뷰포트 크기 관련 문제
- spec/features/issues/filtered_search/filter_issues_spec.rb의 일시적인 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10411
카피바라 JS 드라이버 관련 문제
- AJAX 요청이 발생하지 않을 때 AJAX를 기다리지 않습니다.: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10454
- 비스: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12626
카피바라 기대시간 초과
정지된 스펙
스펙이 정지되거나 CI에서 타임아웃이 발생하는 경우, 로드 인터록 감지 모니터의 교착 상태 버그로 인한 것일 수 있습니다.
진단하기 위해, 다음을 사용할 수 있습니다. sigdump 루비 스레드 덤프를 인쇄합니다:
- 정지된 스펙을 로컬에서 실행합니다.
-
다음 명령어를 실행하여 루비 스레드 덤프를 발생시킵니다:
kill -CONT <pid>
- 스레드 덤프는
/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'
요청을 하기 전에 팩토리를 생성하여 우회한 예시를 확인하세요:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81112
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158890
- https://gitlab.com/gitlab-org/gitlab/-/issues/337039
제안 사항
테스트 파일 분할
문제가 있는 테스트를 식별하고 컨텍스트를 좁히기 위해 큰 RSpec 파일을 여러 파일로 나누는 것이 도움이 될 수 있습니다.
CI에서 작업 실패 재현
CI에서 작업 실패를 재현하는 것은 테스트가 실패하는 이유를 해결하는 데 항상 도움이 됩니다. 같은 세트의 테스트 파일을 동일한 순서로 실행해야 합니다. Knapsack을 사용하여 테스트를 동시에 분산시키기 때문에 두 파이프라인 간에 파일이 다르게 분산될 수 있습니다. 다음 단계로 작업 분산을 하드코딩할 수 있습니다:
- 재현하려는 작업을 찾아서 해당 작업이 실행된 커밋을 식별하고, 로컬
gitlab-org/gitlab
브랜치를 동일한 커밋으로 설정하여 동일한 프로젝트를 실행하도록 합니다. - 작업 로그에서 Knapsack에 의해 분산된 스펙 파일 목록을 찾습니다.
Running command: bundle exec rspec
를 검색하면 이 명령의 마지막 인수에 파일 이름 목록이 포함되어야 합니다. 이 목록을 복사합니다. -
tooling/lib/tooling/parallel_rspec_runner.rb
로 이동하여 테스트 파일 분배가 발생하는 곳을 확인합니다. 이 머지 요청을 예로 삼아, 2단계에서 복사한 파일 목록을TEST_FILES
상수에 저장하고,rspec_command
메서드를 업데이트하여 이 목록을 RSpec가 실행하도록 합니다. -
spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
의 테스트를 건너뛰어 파이프라인이 조기 실패하지 않게 합니다. - 특정 버전에서 파이프라인을 실행하려면 병합된 결과 파이프라인을 실행하고 싶지 않습니다. 이를 위해 MR에 병합 충돌을 도입할 수 있습니다.
- 스펙 순서를 보존하기 위해
spec/support/rspec_order.rb
파일을 업데이트하여 원래 실패한 작업에서 보여지는 값으로Kernel.srand
를 하드코딩합니다. 여기에서와 같이 하세요.Randomized with seed
다음에 이 값이 나오는 것을 검색하여 작업 로그에서 srand 값을 찾을 수 있습니다.
자원
- 불안정한 테스트: 다시 실행하려고 확신하십니까?
- 불안정한 테스트를 처리하고 제거하는 방법
- 당신의 Rails 테스트 스위트에서 불안정성을 다루는 팁
- ‘불안정한’ 테스트: 짧은 이야기
- 테스트 인사이트
느린 테스트
상위 느린 테스트
우리는 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개의 느린 테스트 평균 |