CI 불안정한 테스트 해결: Evil Martians의 포괄적 접근법

Flaky tests, be gone: long-lasting relief for chronic CI retry irritation!

작성자
발행일
2025년 09월 23일

핵심 요약

  • 1 테스트 스위트의 불안정성(Flakiness)은 개발 생산성을 저해하며, 이를 해결하기 위한 '무관용 정책'과 체계적인 접근법이 필수적입니다.
  • 2 단위 테스트의 불안정성은 전역 상태, 데이터베이스 오염, 외부 의존성, 시간 문제, 그리고 부적절한 테스트 설계 등 다양한 원인에서 비롯됩니다.
  • 3 피처 테스트는 브라우저 환경의 특성상 불안정성이 높으므로, 전략적 재시도, 안정적인 브라우저 설정, 견고한 셀렉터 및 JS 동기화 기법을 적용해야 합니다.

도입

CI에서만 실패하고 로컬에서는 통과하는 불안정한 테스트(flaky tests)는 개발자에게 흔한 문제이며, 이는 생산성 저하와 신뢰도 하락으로 이어집니다. Evil Martians는 ClickFunnels의 대규모 테스트 스위트(9천 개 이상의 단위 테스트, 1천 개 이상의 피처 테스트)에서 이러한 불안정성을 해결하여 약 80%의 성공률을 거의 100%로 끌어올린 경험을 공유하며, 만성적인 CI 재시도 문제를 해결할 수 있는 포괄적인 해결책을 제시합니다. 이 글은 테스트 불안정성의 근본 원인을 진단하고 치료하는 'Evil Martians 공식'을 소개합니다.

이 공식은 불안정한 테스트의 모든 알려진 원인을 임상적으로 다루며, 크게 네 가지 핵심 영역으로 나뉩니다.### 1. 무관용 정책 및 격리 시스템* 불안정한 테스트를 CI 파이프라인에서 즉시 격리하고, 팀이 문제를 인지하도록 강제하는 ‘무관용 정책’을 수립합니다.* RSpec의 filter_run_excluding :flaky 설정을 통해 CI 환경에서 불안정한 테스트를 건너뛸 수 있습니다.* config.order = :random 설정으로 테스트 실행 순서를 무작위화하여 숨겨진 의존성을 노출하고, around(:each) 블록을 사용하여 특정 테스트를 여러 번 반복 실행함으로써 불안정성을 빠르게 재현할 수 있습니다.### 2. 단위 테스트 불안정성 원인* 전역 상태: 전역, 클래스, 싱글톤 변수, Rails.cache, RequestStore 등 테스트 간에 상태가 오염되는 문제를 해결하기 위해 각 테스트 후 명확한 정리(cleanup) 전략을 적용합니다. RequestStore.clear!, $debug_mode = false, Rails.cache.clear 등의 방법을 사용합니다.* 전역 설정 및 환경 변수: ENV, I18n.locale, Rails.logger, Rails.application.routes 등 전역 설정이 테스트에 영향을 미치지 않도록 around 블록을 활용하여 테스트 전후에 상태를 복원합니다. Climate Control Gem 사용을 권장합니다.* 시간 문제: travel_to 또는 Timecop Gem을 사용하여 시간을 고정하여 시간 의존적인 테스트의 비결정성을 제거합니다.* 데이터베이스 상태: * 트랜잭션 외부 수정: before(:all) 대신 let_it_be (TestProf)를 사용하여 트랜잭션 내에서 데이터를 생성하고, DDL 작업은 수동으로 정리합니다. * 시드 데이터 및 메모리 오염: TestProf::AnyFixture를 사용하여 시드 데이터를 관리하고 메모리 상태를 새로 고칩니다. CI에서는 데이터베이스 덤프를 캐싱하여 시딩 과정을 최적화할 수 있습니다. * 데이터베이스 정렬: 명시적인 order를 사용하거나 contain_exactly와 같은 순서 무관한 어설션을 사용합니다. * 기본 키 시퀀스: 특정 ID 값에 의존하기보다 관계를 테스트하고, 존재하지 않는 레코드에는 명확히 비현실적인 ID(-1)를 사용합니다.* 외부 시스템 의존성: * 피처 플래그: LaunchDarkly와 같은 피처 플래그 시스템은 프로덕션 덤프 기반의 정교한 모킹 시스템을 구축하여 예측 가능한 동작을 보장합니다. * 외부 데이터 저장소: Redis, Elasticsearch, RabbitMQ 등은 Rails 트랜잭션에 참여하지 않으므로 flushdb, indices.delete, clear_all 등을 통해 각 테스트 후 수동으로 데이터를 정리합니다. * 잡 및 메일러 큐: Sidekiq, ActionMailer 큐는 clear_all을 통해 비웁니다. * 파일 시스템 변경: FileUtils.rm_rf 또는 FakeFS Gem을 사용하여 파일 시스템 변경 사항을 정리합니다. * HTTP 요청 및 외부 API: WebMock으로 외부 HTTP 호출을 방지하고, 복잡한 상호작용은 VCR Gem을 사용하여 녹화/재생합니다. VCR_RERECORD_CASSETTES 환경 변수를 활용하여 카세트 재녹화를 자동화할 수 있습니다. * 기타 네트워크 프로토콜: DNS 조회, 이메일 서비스, 소켓 등도 모킹하여 외부 의존성을 제거합니다.* 문제성 테스트 설계: 너무 무작위적인 데이터 생성, 엄격한 셀렉터, 잘못된 타이밍 가정 등 테스트 자체의 설계 문제로 인한 불안정성을 개선합니다. 빠른 테스트는 불안정성을 줄이는 데 기여합니다.### 3. 피처 테스트 안정성* 브라우저 테스트 재시도: 브라우저 환경의 일시적 불안정성을 고려하여 rspec-retry 또는 Minitest::Retry를 사용하여 피처 테스트에만 제한적으로 재시도를 적용합니다. 단위 테스트에는 재시도를 피해야 합니다.* 필수 브라우저 설정: window_size를 고정하여 뷰포트 일관성을 유지하고, 애니메이션 및 전환 효과를 비활성화하며, Capybara.default_max_wait_time을 늘려 로딩 시간을 확보합니다. CI에서는 자산을 미리 컴파일하고 스크린샷 설정을 통해 실패 원인을 시각적으로 파악합니다.* 브라우저 고급 상태: page.clear_storage, sessionStorage.clear(), localStorage.clear() 등을 통해 테스트 간 브라우저의 고급 상태를 정리합니다.* 안정적인 셀렉터 및 JS 동기화: * 테스트 셀렉터 사용: data-testid와 같은 의미론적 속성을 사용하여 UI 변경에 강한 셀렉터를 만듭니다. * 항상 대기 셀렉터 사용: page.first(".navigation-menu")와 같이 Capybara의 대기 기능을 활용하는 셀렉터를 사용하고, sleep 사용을 지양합니다. * capybara-lockstep Gem: Hotwire, React 등 비동기 웹 애플리케이션에서 Capybara가 모든 JavaScript 비동기 상호작용이 완료될 때까지 기다리도록 하여 UI 상호작용의 신뢰성을 높입니다.* 최종 일관성: 극히 예외적인 경우(예: 실제 잡 처리 포함 스모크 테스트)에만 sleep을 사용하여 시스템 전파 시간을 기다립니다.### 4. 멈춘 테스트 실행 완화* CI에서 테스트가 멈추는(stuck) 문제를 해결하기 위해 sigdump Gem을 사용하여 모든 스레드의 백트레이스를 덤프합니다.* ClickFunnels 사례에서는 다중 데이터베이스와 쿼리 캐시로 인한 Rails 데드락(Rails 7.1+ 업데이트로 해결 예정) 및 타임아웃 보호가 없는 Capybara 매처 루프 문제가 발견되었습니다. 무한 대기를 방지하기 위해 루프 조건에는 항상 타임아웃을 적용해야 합니다.

결론

불안정한 테스트는 단순한 불편함을 넘어 개발팀의 생산성을 심각하게 저해하고 CI에 대한 신뢰를 떨어뜨립니다. Evil Martians의 공식은 이러한 시간 낭비를 완전히 제거하기 위한 체계적인 접근법을 제시합니다. 이 공식은 무관용 정책, 단위 테스트의 전역 상태 및 외부 의존성 정리, 피처 테스트의 안정성 강화, 그리고 멈춘 테스트 실행 완화를 위한 진단 도구 사용을 포함합니다. ClickFunnels의 성공 사례에서 볼 수 있듯이, 이러한 종합적인 전략은 테스트 스위트의 신뢰도를 극적으로 향상시키고 개발자 경험을 개선하는 데 필수적입니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!