취약한 테스트 (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)를 사용합니다. 테스트가 테스트 스위트를 시작할 때 실행되면 충분한 레코드가 생성되지 않으므로 통과할 수 있지만, 테스트가 스위트를 나중에 실행하면 실제로 ID 42인 레코드가 있을 수 있기 때문에 테스트가 실패하기 시작합니다.
  • 예시 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

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

재현 난이도: 쉽게부터 중간까지, 테스트가 특정 날짜 이후에 일관되게 실패하는지, 또는 특정 시간이나 날짜에만 실패하는지에 따라 다릅니다.

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

예시:

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

불안정한 인프라

라벨: flaky-test::unstable infrastructure

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

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

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

예시:

  • 예시 1: 현재 러너가 심한 부하를 받고 있습니다.
  • 예시 2: 러너가 네트워크 문제를 겪어 일찍 작업이 실패합니다.

격리된 테스트

master에 애매하게 동작하는 테스트가 있는 경우:

  1. 해당 그룹 라벨을 사용하여 ~"failure::flaky-test" 이슈를 생성하세요.
  2. 첫 번째 실패 후 테스트를 격리하십시오. 시간 내에 고치지 못하는 경우, 모든 개발자의 생산성에 영향을 미치므로 격리해야 합니다.

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에서 겪었던 문제점

순서 종속성이 있는 변경 가능한 테스트

이러한 변경 가능한 테스트는 다른 테스트와 함께 실행될 때 순서에 따라 실패할 수 있습니다. 예를 들어:

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

  1. 먼저 flaky 테스트 이전에 실행된 테스트 목록을 얻습니다. CI 작업 출력 로그의 Knapsack node specs: 아래에서 목록을 검색할 수 있습니다.
  2. 테스트 목록을 파일로 저장한 다음 다음을 실행하십시오:

    cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
    

순서 종속성 문제가 있다면, 위의 스크립트는 최소 재현을 출력할 것입니다.

시간에 민감한 변경 가능한 테스트

배열 순서 기대값

기능 테스트

Capybara 뷰포트 사이즈 관련 문제

Capybara JS 드라이버 관련 문제

Capybara 예상 시간 제한

멈춰 있는 테스트

만약 테스트가 멈춘다면, Rails의 버그에 의한 것일 수 있습니다:

제안

테스트 파일 분할

문제가 될 수 있는 테스트를 식별하고 범위를 좁히기 위해 대형 RSpec 파일을 여러 파일로 분할하는 것이 도움이 될 수 있습니다.

CI에서 작업 실패 재현하기

CI에서 작업 실패를 재현하는 것은 언제나 테스트가 왜 실패했는지, 그리고 어떻게 실패했는지에 대한 문제 해결에 도움이 됩니다. 이를 위해서는 동일한 테스트 파일을 동일한 순서로 실행해야 합니다. 우리는 테스트를 병렬 작업으로 분산하는 데 Knapsack를 사용하므로 파일은 두 개의 파이프라인 간에 다르게 분산될 수 있습니다. 이 작업 분배를 하드코딩하기 위해 다음 단계를 거칩니다:

  1. 재현하고 싶은 작업을 찾고, 해당 작업이 실행된 커밋을 식별하여 동일한 프로젝트 사본으로 실행하는 것을 보장하기 위해 로컬 gitlab-org/gitlab 브랜치를 해당 커밋으로 설정합니다.
  2. 작업 로그에서 Knapsack에 의해 분산된 테스트 파일 목록을 찾습니다. Running command: bundle exec rspec를 검색하여 이 명령의 마지막 인수에 파일 이름 목록이 포함되어 있어야 합니다. 이 목록을 복사합니다.
  3. 테스트 파일 분배가 발생하는 tooling/lib/tooling/parallel_rspec_runner.rb로 이동합니다. 이 MR를 예로 삼아, 2단계에서 복사한 파일 목록을 TEST_FILES 상수에 저장하고, 예시 MR에서 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를 검색하여 해당 값 뒤에 따라오는 작업 로그에서 srand 값을 찾을 수 있습니다.

자료


테스트 문서로 돌아가기