- 불안정한 테스트란 무엇인가요?
- 테스트가 불안정해지는 잠재적인 원인은 무엇인가요?
- 로컬에서 불안정한 테스트를 어떻게 재현하나요?
- 격리된 테스트
- 자동 재시도 및 플라키 테스트 감지
- GitLab에서 경험한 문제점
- 제안
- 리소스
불안정한 테스트
불안정한 테스트란 무엇인가요?
가끔 실패하는 테스트지만 여러 번 재시도하면 결국 통과하는 테스트입니다.
테스트가 불안정해지는 잠재적인 원인은 무엇인가요?
상태 유출
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
)를 사용하는데, 이 테스트가 테스트 스위트의 시작 부분에서 실행되면 충분한 레코드가 생성되지 않으므로 통과될 수 있지만, 스위트의 나중에 실행되면 실제로 ID42
를 가진 레코드가 있을 수 있기 때문에 테스트가 실패할 수 있습니다. -
예시 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:
불안정한 인프라
레이블: flaky-test::unstable infrastructure
설명: 테스트가 때때로 인프라 문제로 실패합니다.
재현 난이도: 어려움. CI 인프라 문제를 재현하기가 정말 어렵습니다. 로컬에서 컨테이너를 사용하여 가능할 수 있습니다.
해상: 전용 이슈에서 인프라 부서와 대화를 시작하는 것이 일반적으로 좋은 생각입니다.
예시:
로컬에서 불안정한 테스트를 어떻게 재현하나요?
- 로컬에서 실패를 재현합니다.
- CI 작업 로그에서 RSpec
seed
를 찾습니다. - 또는
while :; do bin/rspec <spec> || break; done
을(를) 반복해서 실행하여seed
를 찾습니다.
- CI 작업 로그에서 RSpec
-
bin/rspec --seed <이전에 찾은 시드> --bisect <spec>
로 특정 실패를 이분법적으로 줄입니다. - 남은 예시를 확인하고 상태 누출을 주의합니다.
- 예:
let_it_be
로 생성된 레코드를 업데이트하는 것은 일반적인 문제의 원인입니다.
- 예:
- 수정한 후,
seed
로 테스트를 다시 실행합니다. -
랜덤한 순서에서 테스트를 실행할 수 있는지 확인하기 위해
scripts/rspec_check_order_dependence
를 실행합니다. - 다시 반복으로
while :; do bin/rspec <spec> || break; done
을 실행하여(를) 점심을 먹으면서 플라키하지 않는지 확인합니다.
격리된 테스트
master
에서 불안정한 테스트가 있을 때:
- 관련 그룹 레이블이 있는 ~"failure::flaky-test" 이슈를 생성합니다.
- 첫 번째 실패 후 테스트를 격리합니다. 해당 테스트를 적시에 고칠 수 없는 경우 모든 개발자의 프로덕션성에 영향을 미치므로 격리되어야 합니다.
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에서 경험한 문제점
-
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 - 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
- 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/-/issues/31437 : https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10934
- JS 테스트에서, 요소 이동이 정확한 시간에 일어나면 Capybara가 클릭을 보낼 때 요소가 이동하여 오류 클릭이 발생할 수 있습니다
- 창 크기 및 스크롤 위치로 인해 드롭다운이 올라가거나 내려갑니다: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17660
- 레이지로드된 이미지는 Capybara의 오류 클릭을 유발할 수 있습니다: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18713
- 이벤트 핸들러가 설정되기 전에 JS 이벤트를 트리거합니다: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18742
- Markdown 이미지의
src
속성을 주장할 때 이미지가 레이지로드되기를 기다립니다: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408 - 플래시 공지 배너에 대해 주장하지 마십시오: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79432
Capybara 뷰포트 크기 관련 문제
- spec/features/issues/filtered_search/filter_issues_spec.rb의 일시적 실패: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/29241#note_26743936 : https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10411
Capybara JS 드라이버 관련 문제
- AJAX 요청이 발생하지 않을 때 AJAX를 기다리지 마십시오: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30461: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10454
- Bis: https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34647: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12626
Capybara 예상 시간 초과
- 시간이 지나면 프로젝트 (via Sidekiq)가 시간이 지나면 시간이 60초를 초과하여 가져오기에 시간 초과가 발생합니다: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22599
고정된 스펙
만약 스펙이 멈추면 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
로 이동합니다. 이 Merge Request를 참고로 간단한 예시를 확인한 후, 2단계에서 복사한 파일 디렉터리을TEST_FILES
상수에 저장하고 예시 MR에서rspec_command
메소드를 업데이트하여 이 디렉터리을 실행하도록 합니다. - 파이프라인이 일찍 실패하지 않도록
spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
의 테스트를 건너뜁니다. - 특정 버전에 대해 파이프라인을 실행하도록 하기 위해, Merge된 결과 파이프라인을 실행하고 싶지 않습니다. 이를 위해 MR에 Merge 충돌을 인위적으로 추가할 수 있습니다.
- 특정한 버전에 대한 테스트 순서를 유지하기 위해
spec/support/rspec_order.rb
파일을 업데이트하여Kernel.srand
를 처음 실패한 작업에서 보여진 값으로 하드코딩합니다. 이를 여기에서 참고하실 수 있습니다. 작업 로그에서 검색하여Randomized with seed
뒤에 이 값이 나오는데, 이것이 바로 srand 값입니다.
리소스
- Flaky Tests: 다시 실행하고 싶은가요?
- Flaky Tests 다루기 및 제거하는 방법
- Rails 테스트 스위트에서 Flakiness 처리하는 요령
- ‘Flaky’ tests: 짧은 이야기
- 테스트 인사이트