비정상 테스트
GitLab v19.1이 페이지는 GitLab의 불안정한 테스트(flaky test)를 이해하고 디버깅하기 위한 기술적 참고 자료를 제공합니다. 가끔씩 실패하지만, 충분한 횟수로 재시도하면 결국 통과하는 테스트입니다. CI job 로그에서 RSpec seed를 찾습니다.
비정상 테스트#
불안정한 테스트 (Flaky tests)#
이 페이지는 GitLab의 불안정한 테스트(flaky test)를 이해하고 디버깅하기 위한 기술적 참고 자료를 제공합니다. 불안정한 테스트 관리, 모니터링 및 모범 사례에 관한 프로세스 정보는 Flaky Tests 핸드북 페이지를 참조하세요.
불안정한 테스트란?#
가끔씩 실패하지만, 충분한 횟수로 재시도하면 결국 통과하는 테스트입니다.
불안정한 테스트를 로컬에서 재현하는 방법#
- 로컬에서 실패 재현
CI job 로그에서 RSpec seed를 찾습니다.
-
또는
while :; do bin/rspec <spec> || break; done을 루프로 실행하여seed를 찾습니다. -
다음 명령으로 스펙 실패를 이분 탐색(bisect)하여 예제를 줄입니다.
bin/rspec --seed <이전에 찾은 값> --require ./config/initializers/macos.rb --bisect <spec> -
남은 예제들을 살펴보고 상태 누출(state leakage)을 확인합니다.
예를 들어, let_it_be로 생성된 레코드를 업데이트하는 것은 흔한 문제의 원인입니다.
-
수정 후
seed를 사용하여 스펙을 다시 실행합니다. -
scripts/rspec_check_order_dependence를 실행하여 스펙이 무작위 순서로 실행될 수 있는지 확인합니다. -
while :; do bin/rspec <spec> || break; done을 루프로 다시 실행하여(점심을 먹으며 기다립니다) 더 이상 불안정하지 않은지 확인합니다.
격리된 테스트 (Quarantined tests)#
불안정한 테스트가 master의 개발을 차단하는 경우, 다른 개발자에게 미치는 영향을 방지하기 위해 해당 테스트를 격리해야 합니다.
Test Quarantine Process 핸드북 페이지에서는 다음을 포함한 격리 프로세스에 대한 포괄적인 지침을 제공합니다:
-
빠른 격리 프로세스 또는 장기 격리 프로세스를 사용해야 하는 경우
-
타임라인 기대치 및 소유권 책임
-
격리에서 테스트를 제거하는 방법
-
격리 소유권 및 에스컬레이션 절차 작동 방식
즉각적인 격리가 필요한 경우, 빠른 병합을 위해 빠른 격리 프로세스를 사용하세요. 코드베이스에서 테스트를 격리하는 구현 세부 사항은 핸드북 페이지를 참조하세요.
자동 재시도 및 불안정한 테스트 탐지#
실패한 테스트는 별도의 RSpec 프로세스에서 자동으로 한 번 재시도됩니다.
자세한 내용은 별도 프로세스에서 실패한 테스트 자동 재시도를 참조하세요.
테스트가 불안정해지는 잠재적 원인#
상태 누출 - flaky-test::state leak
설명: 이전 테스트에서 데이터 상태가 누출되었습니다. 실제 원인은 이 불안정한 테스트가 아닐 가능성이 높습니다.
재현 난이도: 보통. 일반적으로 실패하는 것을 재현할 때까지 동일한 스펙 파일을 반복 실행합니다.
해결 방법: 이전 테스트 및/또는 테스트 데이터나 환경이 수정되는 위치를 수정하여 각 테스트 후 원래의 깨끗한 상태로 재설정되도록 합니다.
예시:
-
예시 1: 상태 누출은 테스트 예제 간에 공유된
let_it_be로 생성된 데이터 레코드에서 발생할 수 있으며, 일부 테스트가 의도적으로 또는 비의도적으로 모델을 수정하여 테스트 예제에서 동기화되지 않은 데이터가 발생할 수 있습니다. 이로 인해 후속 테스트 예제나 재시도에서PG::QueryCanceled: ERROR가 발생할 수 있습니다. 상태 누출 및 해결 방법에 대한 자세한 내용은 GitLab 테스트 모범 사례를 참조하세요. -
예시 2: 마이그레이션 테스트가 데이터베이스를 롤백하고 테스트를 수행한 후 비일관적인 상태로 데이터베이스를 롤업할 수 있으며, 이로 인해 후속 테스트가 특정 칼럼을 인식하지 못할 수 있습니다.
-
예시 3: 한 테스트가 후속 테스트에서 사용되는 데이터를 수정합니다.
-
예시 4: 데이터베이스 쿼리에 대한 테스트가 새로운 데이터베이스에서는 통과하지만, 이전 테스트 시퀀스를 처리하는 데 데이터베이스가 사용된 CI/CD 파이프라인에서는 실패합니다. 이는 쿼리 자체가 깨끗하지 않은 데이터베이스에서 작동하도록 업데이트되어야 함을 의미할 가능성이 높습니다.
-
예시 5: 비동기 요청의 관련 없는 데이터베이스 연결이 체크백되어 테스트가 실수로 이러한 관련 없는 데이터베이스 연결을 사용하게 되었습니다. 이 실패는 이 머지 리퀘스트에서 해결되었습니다.
-
예시 6: 데이터베이스 연결의 최대 생존 시간(TTL)으로 인해 이러한 연결이 끊어지며, 이로 인해 이러한 연결의 트랜잭션에 의존하는 테스트가 차례로 실패합니다. 이 이슈는 이 머지 리퀘스트에서 수정되었습니다.
-
예시 7: 테스트에서 사용된 TCP 소켓이 다음 테스트 전에 닫히지 않았으며, 다음 테스트도 동일한 포트를 다른 TCP 소켓으로 사용했습니다.
-
예시 8:
let_it_be가before블록에 정의된 스텁에 의존했습니다.let_it_be는before(:all)중에 실행되므로 스텁이 아직 설정되지 않았습니다. 이로 인해 테스트가 실제 메서드 호출에 노출되었으며, 해당 메서드는 메서드 캐시를 사용했습니다.
데이터셋 특이적 - flaky-test::dataset-specific
설명: 테스트가 데이터셋이 특정(보통 제한된) 상태 또는 순서에 있다고 가정하며, 이는 테스트 스위트 내에서 테스트가 언제 실행되느냐에 따라 사실이 아닐 수 있습니다.
재현 난이도: 보통. 이슈를 재현하는 데 필요한 데이터 양이 로컬에서 달성하기 어려울 수 있습니다. 순서 이슈는 테스트를 여러 번 반복 실행하여 재현하기가 더 쉽습니다.
해결 방법:
-
데이터셋이 특정 상태에 있다고 가정하지 않도록 테스트를 수정하고, ID를 하드코딩하지 마세요.
-
테스트가 순서가 아닌 요소에만 관심이 있는 경우 어서션을 느슨하게 하세요.
-
결정적 순서를 지정하여 테스트를 수정하세요.
-
결정적 순서를 지정하여 앱 코드를 수정하세요.
예시:
-
예시 1: 테이블에 500개 이상의 칼럼이 있을 때 데이터베이스가 재생성됩니다. 머지 리퀘스트에서는 통과할 수 있지만, 테스트 순서가 변경되면 나중에
master에서 실패할 수 있습니다. -
예시 2: 존재하지 않는 ID로 레코드를 찾으려 할 때 오류 메시지가 반환되는지 테스트가 어서트합니다. 테스트는 존재하지 않아야 하는 하드코딩된 ID(예:
42)를 사용합니다. 테스트가 스위트 초반에 실행되면 충분한 레코드가 생성되지 않아 통과할 수 있지만, 나중에 스위트에서 실행되면 실제로 ID42를 가진 레코드가 있을 수 있으므로 테스트가 실패하기 시작할 수 있습니다. -
예시 3:
ORDER BY를 지정하지 않으면 데이터베이스에 결정적 순서가 부여되지 않거나 테스트에서 데이터 경쟁이 발생할 수 있습니다. -
예시 4.
SQL 쿼리 과다 - flaky-test::too-many-sql-queries
설명: SQL 쿼리 제한에 도달하여 Gitlab::QueryLimiting::Transaction::ThresholdExceededError가 트리거됩니다.
재현 난이도: 보통. 이 실패는 스펙 순서에 따라 영향을 받을 수 있는 쿼리 캐시 상태에 의존할 수 있습니다.
해결 방법: 쿼리 수 제한 문서를 참조하세요.
무작위 입력 - flaky-test::random input
설명: 테스트가 무작위 값을 사용하며, 때로는 기대값과 일치하고 때로는 그렇지 않습니다.
재현 난이도: 쉬움. 테스트가 실패한 시점에 사용된 "무작위 값"을 사용하도록 로컬에서 테스트를 수정할 수 있습니다.
해결 방법: 문제가 재현되면 테스트 또는 앱을 디버깅하고 수정하기가 비교적 쉽습니다.
예시:
- 예시 1: 데이터 입력이 무작위이기 때문에 간헐적으로만 나타나는 특정 데이터를 처리할 만큼 테스트가 견고하지 않습니다.
신뢰할 수 없는 DOM 셀렉터 - flaky-test::unreliable dom selector
설명: 테스트에 사용된 DOM 셀렉터가 신뢰할 수 없습니다.
재현 난이도: 보통~어려움. DOM 셀렉터가 중복되어 있는지 또는 지연 후에 나타나는지 등에 따라 다릅니다. API 또는 컨트롤러에 지연을 추가하면 이슈를 재현하는 데 도움이 될 수 있습니다.
해결 방법: 실제로 문제에 따라 다릅니다. 요청이 완료될 때까지 기다리거나 페이지를 스크롤하는 것 등이 해결 방법이 될 수 있습니다.
예시:
-
예시 1: 하나 이상의 요소와 일치하는 고유하지 않은 CSS 셀렉터 또는
element not found오류를 발생시키기 전에 렌더링 시간을 허용하지 않는 대기하지 않는 셀렉터 메서드. -
예시 2: GraphQL 요청이 완료되고 UI가 업데이트된 후에만 CSS 셀렉터가 나타납니다.
-
예시 3: 거짓 양성(false-positive) 테스트로, Capybara가 페이지 방문 후 즉시 true를 반환하여 페이지가 완전히 로드되지 않았거나 요소가 webdriver에 의해 감지될 수 없는 경우(뷰포트 외부에 렌더링되거나 다른 요소 뒤에 가려진 경우 등).
날짜/시간 민감 - flaky-test::datetime-sensitive
설명: 테스트가 특정 날짜 또는 시간을 가정합니다.
재현 난이도: 쉬움~보통. 테스트가 특정 날짜 이후 지속적으로 실패하는지, 아니면 특정 시간 또는 날짜에만 실패하는지에 따라 다릅니다.
해결 방법: 시간을 고정(freezing)하는 것이 일반적으로 좋은 해결 방법입니다.
예시:
불안정한 인프라 - flaky-test::unstable infrastructure
설명: 인프라 이슈로 인해 테스트가 간헐적으로 실패합니다.
재현 난이도: 어려움. CI 인프라 이슈를 재현하기는 매우 어렵습니다. 컨테이너를 로컬에서 사용하면 가능할 수도 있습니다.
해결 방법: 전용 이슈에서 인프라 부서와 대화를 시작하는 것이 일반적으로 좋은 방법입니다.
예시:
부적절한 동기화 - flaky-test::improper synchronization
설명: 지연, 최종 일관성(eventual consistency), 비동기 작업 또는 경쟁 조건(race condition)과 같은 타이밍 관련 요인으로 인해 발생하는 불안정한 테스트 이슈입니다.
이러한 이슈는 테스트 로직, 테스트 대상 시스템 또는 이들의 상호작용에서 발생하는 단점에 기인할 수 있습니다.
테스트가 향상된 동기화를 통해 이러한 이슈를 해결하는 경우도 있지만, 해결이 필요한 기반 시스템 버그를 드러내는 경우도 있습니다.
재현 난이도: 보통. 예를 들어, 기능 테스트에서 아직 렌더링되지 않은 페이지의 요소를 참조하거나, 단위 테스트에서 비동기 작업이 완료될 때까지 기다리지 못함으로써 재현할 수 있습니다.
해결 방법: 엔드투엔드 테스트 스위트에서 eventually 매처 사용.
예시:
추가 디버깅 기법#
테스트 파일 분할#
컨텍스트를 좁히고 문제가 있는 테스트를 식별하기 위해 큰 RSpec 파일을 여러 파일로 분할하면 도움이 될 수 있습니다.
CI에서 동일한 테스트 파일 세트로 job 실패 재현#
CI에서 job 실패를 재현하면 테스트가 왜 그리고 어떻게 실패하는지 트러블슈팅하는 데 항상 도움이 됩니다. 이를 위해서는 동일한 스펙 순서로 동일한 테스트 파일을 실행해야 합니다. 병렬화된 job에 테스트를 분배하기 위해 Knapsack을 사용하므로, 두 파이프라인 간에 파일이 다르게 분배될 수 있습니다. 다음 단계를 통해 이 job 분배를 하드코딩할 수 있습니다:
-
재현하려는 job을 찾고, 해당 job이 실행된 커밋을 식별한 다음, 동일한 프로젝트 복사본으로 실행하고 있는지 확인하기 위해 로컬
gitlab-org/gitlab브랜치를 동일한 커밋으로 설정합니다. -
job 로그에서 Knapsack이 분배한 스펙 파일 목록을 찾습니다.
Running command: bundle exec rspec을 검색하면 이 명령의 마지막 인수에 파일 이름 목록이 포함되어 있어야 합니다. 이 목록을 복사합니다. -
테스트 파일 분배가 발생하는
tooling/lib/tooling/parallel_rspec_runner.rb로 이동합니다. 예시로 이 머지 리퀘스트를 참조하세요. 2단계에서 복사한 파일 목록을TEST_FILES상수에 저장하고 예시 MR에서처럼rspec_command메서드를 업데이트하여 RSpec이 이 목록을 실행하도록 합니다. -
spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb의 테스트를 건너뛰어 파이프라인이 조기에 실패하지 않도록 합니다. -
특정 버전에 대해 파이프라인을 강제로 실행하려는 것이므로 병합 결과 파이프라인을 실행하지 않습니다. MR에 병합 충돌을 도입하여 이를 달성할 수 있습니다.
-
스펙 순서를 유지하려면
spec/support/rspec_order.rb파일을 업데이트하여 머지 리퀘스트 128428에서처럼Kernel.srand를 원래 실패한 job에 표시된 값으로 하드코딩합니다.Randomized with seed를 검색하면 job 로그에서srand값을 찾을 수 있으며, 이 값 뒤에 해당 값이 나타납니다.
순서 의존적 불안정한 테스트 재현#
단일 파일의 순서 이슈를 식별하려면 불안정한 테스트를 로컬에서 재현하는 방법을 참조하세요.
일부 불안정한 테스트는 다른 테스트와 함께 실행되는 순서에 따라 실패할 수 있습니다. 예를 들어:
서로 다른 파일 간의 순서 이슈를 식별하려면 scripts/rspec_bisect_flaky를 사용하면 됩니다.
이를 통해 실패를 재현하기 위한 최소한의 테스트 조합을 얻을 수 있습니다:
불안정한 테스트 이전에 실행된 스펙 목록을 먼저 확보합니다. CI job 출력 로그의 Knapsack node specs: 아래에서 목록을 검색할 수 있습니다.
스펙 목록을 파일로 저장한 다음 실행합니다:
cat knapsack_specs.txt | xargs scripts/rspec_bisect_flaky
순서 의존성 이슈가 있는 경우 위 스크립트가 최소 재현 방법을 출력합니다.
지표 및 추적#
느린 테스트#
가장 느린 테스트#
ClickHouse 데이터베이스에서 테스트 실행 시간에 대한 정보를 수집합니다. 데이터는 다음 Grafana 대시보드를 사용하여 시각화됩니다.
느린 테스트의 일반적인 패턴#
다음 패턴은 체계적인 개선 노력 중에 확인된 테스트 느림의 가장 일반적인 원인입니다. 각 항목은 테스트 모범 사례 가이드의 자세한 지침으로 연결됩니다.
부재 확인에 긍정적 술어 사용 - 요소가 없을 때 page.has_link?(...)는
전체 기본 타임아웃을 기다립니다. 대신 부정형
(has_no_link? 또는 expect(page).to have_no_link(...))을 사용하세요. 부재할 것으로 예상하는 요소 기다리기 방지를 참조하세요.
find() 대신 all() 사용 — all()은 누락된 요소에서 예외를 발생시키지 않으며 Capybara의 스마트 대기의 혜택을 받지 못합니다. all() 결과에 대한 블록 반복은 특히 느립니다. .first 또는 블록 반복과 함께 all() 사용 방지를 참조하세요.
광범위한 포함을 가진 느린 공유 예제 — 느린 공유 예제는 이를 포함하는 모든 파일에 걸쳐 비용이 배가됩니다. 느린 공유 예제의 성능 영향을 참조하세요.
실제 외부 작업 트리거 — 바이너리 컴파일이나 Git 명령 실행을 위해 셸로 나가는 스펙은 로직이 이미 단위 테스트된 경우에도 해당 실제 시간 비용을 상속합니다. 비용이 많이 드는 외부 작업 모킹을 참조하세요.
팩토리 연쇄 — 불필요하게 깊은 팩토리 연관은 데이터베이스 쓰기를 자동으로 배가시킵니다. 팩토리 사용 최적화를 참조하세요.
불필요한 :js 태그 — HTML 응답으로 충분한 경우에도 전체 JavaScript 브라우저로 스펙을 실행합니다. 필요하지 않은 기능 요청하지 않기를 참조하세요.