취약한 테스트 (Flaky tests)
취약한 테스트(flaky test)란 무엇인가요?
가끔은 실패하는 테스트로, 여러 번 재시도하면 결국 통과하는 테스트입니다.
테스트가 취약해진 원인은 무엇인가요?
상태 누수 (State leak)
Label: flaky-test::state leak
Description: 데이터 상태가 이전 테스트에서 유출되었습니다. 실제 원인은 아마도 여기서 취약한 테스트가 아닙니다.
복원 난이도: 보통입니다. 일반적으로 동일한 스펙 파일을 실행하여 실패하는 테스트를 다시 실행하면 문제가 발생합니다.
해결책: 이전 테스트 및/또는 테스트 데이터 또는 환경이 수정되는 곳을 수정하여 각 테스트 후에 테스트를 깨끗한 상태로 초기화합니다.
예시:
-
예시 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: 테스트는 데이터셋이 특정(보통 제한된) 상태 또는 순서에 있는 것으로 가정합니다. 이는 테스트 스위트 내에서 테스트 실행 시간에 따라 참이 아닐 수 있습니다.
복원 난이도: 중간 수준입니다. 문제를 재현하기 위해 필요한 데이터 양이 로컬에서 어렵게 얻을 수 있을 수 있습니다. 순서 문제는 테스트를 몇 번 실행함으로써 쉽게 재현될 수 있습니다.
해결책:
- 데이터셋이 특정 상태에 있다고 가정하는 테스트를 수정하여 ID를 하드코딩하지 않도록 수정합니다.
- 테스트가 순서에 관계 없이 원소만을 중요시해야 한다면 단언을 완화합니다.
- 결정적인 순서를 지정하여 테스트를 수정합니다.
- 앱 코드를 수정하여 결정적인 순서를 지정합니다.
예시:
-
예시 1: 테이블 중 하나에 500개 이상의 열이 있으면 데이터베이스가 다시 생성됩니다. 테스트 순서가 변경되면 머지 요청에서 통과할 수 있지만 나중에
master
에서 실패할 수 있습니다. -
예시 2: 테스트는 존재하지 않는 ID의 레코드를 찾으려고 시도하는 것을 확인합니다. 테스트는 존재하지 않는 ID(예:
42
)를 사용합니다. 테스트가 테스트 스위트를 시작할 때 실행되면 충분한 레코드가 생성되지 않으므로 통과할 수 있지만, 테스트가 스위트를 나중에 실행하면 실제로 ID42
인 레코드가 있을 수 있기 때문에 테스트가 실패하기 시작합니다. -
예시 3:
ORDER BY
를 지정하지 않으면 데이터베이스에 결정적인 순서가 없거나 테스트에서 데이터 레이스가 발생할 수 있습니다. - 예시 4.
무작위 입력
라벨: flaky-test::random input
설명: 테스트는 때때로 예상과 일치하고 때로는 그렇지 않은 무작위 값들을 사용합니다.
재현 난이도: 테스트가 실패했을 때 “무작위 값”을 사용하도록 로컬에서 쉽게 수정할 수 있습니다.
해결: 문제가 재현될 경우, 테스트 또는 응용 프로그램을 쉽게 디버그하고 수정할 수 있어야 합니다.
예시:
- 예시 1: 특정 데이터를 처리할만큼 테스트가 견고하지 못한 경우, 데이터 입력이 무작위이기 때문에 때로는 나타나기도 합니다.
신뢰할 수 없는 DOM 선택기
라벨: flaky-test::unreliable dom selector
설명: 테스트에서 사용된 DOM 선택기가 믿을 수 없습니다.
재현 난이도: 중간에서 어려움. DOM 선택기가 복제되었는지, 또는 지연 후 나타나는지에 따라 다릅니다. API 또는 컨트롤러에 지연을 추가하면 이슈를 재현하는 데 도움이 될 수 있습니다.
해결: 여기서 정말 문제에 따라 다릅니다. 요청이 완료될 때까지 기다리거나 페이지를 아래로 스크롤하는 등 일 수 있습니다.
예시:
-
예시 1: 고유하지 않은 CSS 선택기가 하나 이상의 요소와 일치하거나,
element not found
오류를 발생하기 전에 렌더링 시간을 허용하지 않는 비대기 선택기 방법. - 예시 2: CSS 선택기가 GraphQL 요청이 완료된 후에만 나타나며 UI가 업데이트된 경우.
- 예시 3: 잘못된 양성 테스트, Capybara가 페이지 방문 후 즉시 true를 반환하고 페이지가 완전히 로드되지 않았거나 요소가 webdriver에 의해 감지되지 않는 경우(뷰포트 바깥이나 다른 요소 뒤에 렌더링된 경우).
날짜 및 시간 민감
라벨: flaky-test::datetime-sensitive
설명: 테스트는 특정 날짜나 시간을 가정합니다.
재현 난이도: 쉽게부터 중간까지, 테스트가 특정 날짜 이후에 일관되게 실패하는지, 또는 특정 시간이나 날짜에만 실패하는지에 따라 다릅니다.
해결: 시간을 고정하는 것이 일반적으로 좋은 해결책입니다.
예시:
불안정한 인프라
라벨: flaky-test::unstable infrastructure
설명: 테스트는 때때로 인프라 문제로 인해 실패합니다.
재현 난이도: 어려움. CI 인프라 문제를 재현하기는 정말 어렵습니다. 로컬에서 컨테이너를 사용하여 가능할 수도 있습니다.
해결: 전용 이슈에서 인프라 부서와 대화를 시작하는 것이 보통 좋은 아이디어입니다.
예시:
격리된 테스트
master
에 애매하게 동작하는 테스트가 있는 경우:
- 해당 그룹 라벨을 사용하여 ~"failure::flaky-test" 이슈를 생성하세요.
- 첫 번째 실패 후 테스트를 격리하십시오. 시간 내에 고치지 못하는 경우, 모든 개발자의 생산성에 영향을 미치므로 격리해야 합니다.
RSpec
빠른 격리
파이프라인을 기다리지 않고 빠르게 테스트를 격리하려면 빠른 격리 프로세스를 따를 수 있습니다.
장기 격리
테스트가 빠르게 격리된 후, 장기 격리 프로세스를 진행할 수 있습니다. 이를 위해 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
이렇게 하면 CI에서 건너뛰어집니다. 기본적으로 격리된 테스트는 로컬에서 실행됩니다.
로컬 개발 시 --tag ~quarantine
와 함께 실행하여 그것들을 건너뛸 수 있습니다.
bin/rspec --tag ~quarantine
장기 격리 MR이 프로덕션에 도달하면 이전에 만든 빠른 격리 MR을 되돌려야합니다.
기능 범주별로 격리된 테스트 찾기
특정 기능 범주에 대한 모든 격리된 테스트를 찾으려면 ripgrep
를 사용하세요:
rg -l --multiline -w "(?s)feature_category:\s+:global_search.+quarantine:"
Jest
Jest 스펙의 경우 .skip
메서드와 함께 jest/no-disabled-tests
ESLint 규칙을 비활성화하기 위해 eslint-disable-next-line
주석을 사용할 수 있습니다. 여기에 예시가 있습니다:
// 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
두 테스트 프레임워크 모두에 대해 “격리된 테스트” 라벨을 이슈에 추가해야 합니다.
테스트가 격리되면 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에서 겪었던 문제점
- 일부 API 스펙이 실패할 때
rspec-retry
가 문제가 되고 있습니다: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9825 -
PG::UniqueViolation
로 인한 때때로 발생하는 RSpec 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/28307#note_24958837 - 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
를 사용할 수 있으며, 이는 실패를 복제하는 최소한의 테스트 조합을 제공합니다:
- 먼저 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가 클릭을 잘못 보낼 수 있음
- 이벤트 핸들러 설정 전에 JS 이벤트를 트리거
- Markdown 이미지의
src
속성에 대해 레이지로드된 이미지를 단언할 때 이미지 로드를 대기 - 플래시 알림 배너에 대해 단언하지 말기
Capybara 뷰포트 사이즈 관련 문제
- spec/features/issues/filtered_search/filter_issues_spec.rb의 일시적 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10411
Capybara JS 드라이버 관련 문제
- AJAX 요청이 없는 경우 AJAX를 기다리지 마십시오: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10454
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12626
Capybara 예상 시간 제한
멈춰 있는 테스트
만약 테스트가 멈춘다면, Rails의 버그에 의한 것일 수 있습니다:
- https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81112
- 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
로 이동합니다. 이 MR를 예로 삼아, 2단계에서 복사한 파일 목록을TEST_FILES
상수에 저장하고, 예시 MR에서rspec_command
메서드를 업데이트하여 이 목록을 실행하도록 합니다. - 파이프라인이 일찍 실패하지 않도록
spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
에서 테스트를 건너뜁니다. - 특정 버전에 대해 파이프라인을 실행하도록 강제하고자 하므로, 병합된 결과 파이프라인을 실행하지 않습니다. 이를 위해 해당 MR에 병합 충돌을 도입할 수 있습니다.
- 테스트 순서를 유지하기 위해
spec/support/rspec_order.rb
파일을 업데이트하여, 원래 실패한 작업에서 표시된 값으로Kernel.srand
를 하드코딩합니다.Randomized with seed
를 검색하여 해당 값 뒤에 따라오는 작업 로그에서 srand 값을 찾을 수 있습니다.
자료
- Flaky Tests: Are You Sure You Want to Rerun Them?
- How to Deal With and Eliminate Flaky Tests
- Tips on Treating Flakiness in your Rails Test Suite
- ‘Flaky’ tests: a short story
- Test Insights