테스트 최적의 수행 방법

테스트 디자인

GitLab에서의 테스트는 우선순위가 높은 것으로 여겨지며, 단순히 뒷편에 놓이는 것이 아닙니다. 특징의 디자인만큼이나 테스트의 디자인을 고려하는 것이 중요합니다.

기능을 구현할 때, 올바른 능력을 올바른 방식으로 개발하는 데에 대해 생각합니다. 이것은 우리의 범위를 관리 가능한 수준으로 좁히는 데 도움이 됩니다. 기능을 구현할 때, 올바른 테스트를 개발하는 것은 중요하지만, 그런 다음에 테스트가 실패할 수 있는 중요한 방법을 모두 고려해야 합니다. 이것은 우리의 범위를 빠르게 관리하기 어려운 수준으로 넓힐 수 있습니다.

테스트 휴리스틱은 이 문제를 해결하는 데 도움이 될 수 있습니다. 이것들은 우리의 코드에서 버그가 나타나는 일반적인 방법들을 간결하게 다룹니다. 테스트를 디자인할 때 알려진 테스트 휴리스틱을 검토하는 시간을 가지는 것이 중요합니다. Handbook의 테스트 엔지니어링 섹션에서 유용한 휴리스틱을 찾을 수 있습니다.

RSpec

RSpec 테스트를 실행하려면:

# 파일에 대한 테스트 실행
bin/rspec spec/models/project_spec.rb

# 해당 파일의 10번째 라인에 대한 테스트 실행
bin/rspec spec/models/project_spec.rb:10

# 문자열을 포함한 예제 이름에 대한 테스트 실행
bin/rspec spec/models/project_spec.rb -e associations

# 모든 테스트 실행, GitLab 코드베이스에는 시간이 많이 소요됩니다!
bin/rspec

변경 사항을 지속적으로 모니터링하고 일치하는 테스트만 실행하려면 Guard를 사용하세요:

bundle exec guard

spring과 guard를 함께 사용하는 경우, 대신 SPRING=1 bundle exec guard를 사용하여 spring을 활용하세요.

일반적인 지침

  • 단일 상위 RSpec.describe ClassName 블록을 사용합니다.
  • 클래스 메서드를 설명하기 위해 .method를 사용하고, 인스턴스 메서드를 설명하기 위해 #method를 사용합니다.
  • 분기 로직을 테스트하기 위해 context를 사용합니다 (RSpec/AvoidConditionalStatements Rubocop Cop - MR).
  • 테스트의 순서를 클래스의 순서와 일치시키려고 노력합니다.
  • Four-Phase Test 패턴을 따르려고 노력하며, 새 줄을 사용하여 단계를 분리합니다.
  • 'localhost'를 하드코딩하는 대신 Gitlab.config.gitlab.host를 사용합니다.
  • 연속으로 생성된 속성의 절대 값에 대해 단언하지 않습니다(자세한 내용은 Gotchas 참조).
  • expect_any_instance_of 또는 allow_any_instance_of를 사용하지 않습니다(자세한 내용은 Gotchas 참조).
  • 기본값이므로 :each 인수를 훅에 제공하지 않습니다.
  • beforeafter 훅에서 :all 대신 :context 범위로 제한하는 것을 선호합니다.
  • 주어진 요소에 작용하는 evaluate_script("$('.js-foo').testSomething()") (또는 execute_script)를 사용할 때, 사전에 Capybara matcher(예: find('.js-foo'))를 사요해 요소가 실제로 존재하는지 확인합니다.
  • 실행하고자 하는 스펙 일부를 격리시키기 위해 focus: true를 사용합니다.
  • 하나 이상의 기대치가 있는 경우 :aggregate_failures를 사용합니다.
  • 빈 테스트 설명 블록에 대해, 테스트가 자명한 경우 specify 대신 it do를 사용합니다.
  • 실제로 존재하지 않는 ID/IID/access level이 필요한 경우 non_existing_record_id/non_existing_record_iid/non_existing_record_access_level를 사용합니다. 123, 1234 또는 심지어 999를 사용하는 것은 이러한 ID가 실제로 데이터베이스에 존재할 수 있기 때문에 부서지기 쉽습니다.

응용 프로그램 코드의 즉시 로딩

기본적으로 응용 프로그램 코드:

  • test 환경에서는 즉시로드되지 않습니다.
  • CI/CD(when ENV['CI'].present?)에서는 즉시로드되어 잠재적인 로딩 문제를 확인합니다.

테스트를 실행할 때 즉시 로딩을 활성화해야 하는 경우, GITLAB_TEST_EAGER_LOAD 환경 변수를 사용하세요:

GITLAB_TEST_EAGER_LOAD=1 bin/rspec spec/models/project_spec.rb

로드된 응용 프로그램 코드에 의존하는 경우 :eager_load 태그를 추가하세요. 이렇게 하면 테스트 실행 전에 응용 프로그램 코드가 즉시로드됨을 보장합니다.

루비 경고

우리는 테스트를 실행할 때 기본적으로 중요 경고를 활성화했습니다. 이러한 경고를 개발자에게 더 가시적으로 만들어주는 것은 더 높은 루비 버전으로 업그레이드하는 데 도움이 됩니다.

환경 변수 SILENCE_DEPRECATIONS를 설정하여 중요 경고를 무시할 수 있습니다. 예:

# 모든 중요 경고를 무시
SILENCE_DEPRECATIONS=1 bin/rspec spec/models/project_spec.rb

테스트 주문

새로운 스팩 파일은 임의의 순서로 실행되어 테스트 주문에 의존하는 플래키 테스트를 드러내는데 사용됩니다.

랜덤화될 때:

  • 예제 그룹 설명 아래에 # order random 문자열이 추가됩니다.
  • 사용된 시드는 테스트 스위트 요약 아래의 스펙 출력에 표시됩니다. 예를 들어, Randomized with seed 27443.

정의된 순서로 여전히 실행되는 스펙 파일 목록은 rspec_order_todo.yml에서 확인할 수 있습니다.

스팩 파일을 임의의 순서로 실행하려면, 다음과 같이 명령어를 사용하여 그들의 순서 의존성을 확인하세요:

scripts/rspec_check_order_dependence spec/models/project_spec.rb

만약 스펙이 확인을 통과하면 해당 스크립트가 자동으로 rspec_order_todo.yml에서 그들을 제거합니다.

만약 스펙이 확인을 통과하지 못하면, 그들이 임의의 순서로 실행되기 전에 수정되어야 합니다.

테스트 속도

GitLab은 병렬화 없이 수 시간이 걸릴 수 있는 방대한 테스트 스위트를 보유하고 있습니다. 정확하고 효과적인 테스트를 작성하며 빠르게 실행되도록 노력하는 것이 중요합니다.

테스트 성능은 품질과 속도를 유지하는 데 중요하며 CI 빌드 시간 및 고정 비용에 직접적인 영향을 미칩니다. 우리는 철저하고 올바르며 빠른 테스트를 원합니다. 여기에서는 이를 달성하기 위해 사용할 수 있는 도구와 기술에 대한 정보를 찾을 수 있습니다.

필요하지 않은 기능을 요청하지 마세요

예제나 상위 컨텍스트를 주석 처리하여 우리의 예제에 기능을 쉽게 추가할 수 있습니다. 이러한 기능의 예는 다음과 같습니다:

  • feature 스펙에서의 :js는 완전한 JavaScript 호환 무방화된 브라우저를 실행합니다.
  • :clean_gitlab_redis_cache는 예제에 깨끗한 Redis 캐시를 제공합니다.
  • :request_store는 예제에 요청 저장소를 제공합니다.

우리는 테스트 종속성을 줄이고, 또한 기능을 피함으로써 설정할 필요가 있는 양을 줄일 수 있습니다.

:js는 피하는 것이 특히 중요합니다. 이는 브라우저의 JavaScript 반응성이 필요한 경우에만 사용되어야 합니다(예: Vue.js 컴포넌트를 클릭하는 경우). 무방화된 브라우저를 사용하는 것은 앱에서 HTML 응답을 구문 분석하는 것보다 훨씬 느립니다.

프로파일링: 테스트가 어디에 시간을 소비하는지 확인하세요

rspec-stackprof을 사용하여 테스트가 어디에 시간을 소비하는지를 보여주는 플레임 그래프를 생성할 수 있습니다.

이 젬은 우리가 https://www.speedscope.app에 업로드할 수 있는 JSON 보고서를 생성합니다.

설치

stackprof 젬은 이미 GitLab에 설치되어 있습니다, 또한 JSON 보고서를 생성하는 스크립트도 사용할 수 있습니다(bin/rspec-stackprof).

# 선택 사항: `speedscope` 패키지를 설치하여 https://www.speedscope.app에 JSON 보고서를 쉽게 업로드할 수 있습니다
npm install -g speedscope
JSON 보고서 생성
bin/rspec-stackprof --speedscope=true <your_slow_spec>
# 스크립트가 종료되면 보고서의 이름이 표시됩니다.

# JSON 보고서를 speedscope.app에 업로드하세요
speedscope tmp/<your-json-report>.json
플레임그래프 해석 방법

다음은 플레임그래프를 해석하고 탐색하는 데 유용한 팁입니다:

  • 플레임그래프에는 여러 가지 뷰가 있습니다. 함수 호출이 많을 때는 Left Heavy가 특히 유용합니다(예: feature 스펙).
  • 확대 또는 축소할 수 있습니다! 탐색 문서를 참조하세요.
  • 느린 feature 테스트를 작업 중이라면, 검색에서 Capybara::DSL#을 검색하여 capybara 동작을 보고 그 소요시간을 확인할 수 있습니다!

분석 사례에 대한 자세한 내용은 #414929 또는 #375004를 참조하세요.

팩토리 사용 최적화

느린 테스트의 흔한 원인은 물체를 지나치게 생성하고, 따라서 계산 및 DB 시간을 소모하는 것입니다. 팩토리는 개발에 필수적이지만, 데이터를 삽입하기 쉬워서 최적화할 수 있을지도 모릅니다.

여기서 염두에 둘 두 가지 기본적인 기술은 다음과 같습니다:

  • 줄이기: 물체를 만드는 것을 피하고, 그것들을 영속화하는 것을 피하십시오.
  • 재사용: 특히 조사하지 않는 중첩된 공유 객체는 일반적으로 공유될 수 있습니다.

생성을 피하기 위해서는 다음 사항을 염두에 두는 것이 가치가 있습니다:

  • instance_doublespyFactoryBot.build(...)보다 빠릅니다.
  • FactoryBot.build(...).build_stubbed.create보다 빠릅니다.
  • .create 대신에 build, build_stubbed, attributes_for, spy, 또는 instance_double를 사용하세요. 데이터베이스 영속화는 느립니다!

데이터베이스 영속성이 필요하지 않은 경우를 찾아내는 데는 Factory Doctor를 사용할 수 있습니다.

팩토리의 최적화 예시 1, 2.

# 경로에 대한 테스트 실행
FDOC=1 bin/rspec spec/[path]/[to]/[spec].rb

create 대신에 buildbuild_stubbed를 사용하는 일반적인 변경 사례입니다:

# 예전
let(:project) { create(:project) }

# 새로운
let(:project) { build(:project) }

Factory Profiler를 사용하여 팩토리를 통한 반복된 데이터베이스 영속성을 식별할 수 있습니다.

# 경로에 대한 테스트 실행
FPROF=1 bin/rspec spec/[path]/[to]/[spec].rb

# 플레임그래프로 시각화
FPROF=flamegraph bin/rspec spec/[path]/[to]/[spec].rb

많은 팩토리를 생성하는 흔한 원인은 팩토리 카스케이드입니다. 팩토리가 연관성을 계속해서 생성하고 다시 생성할 때 발생합니다. 이러한 것들은 total timetop-level time 숫자 간의 뚜렷한 차이로 식별할 수 있습니다:

   total   top-level     total time      time per call      top-level time               name

     208           0        9.5812s            0.0461s             0.0000s          namespace
     208          76       37.4214s            0.1799s            13.8749s            project

위의 표는 우리가 명시적으로 어떤 namespace 물체를 생성하지 않음을 보여줍니다(top-level == 0) - 모두 자동으로 생성됩니다. 하지만 여전히 우리는 208개의 그것들을 가지고 있고, 이것은 9.5초가 걸립니다.

명명된 부모 연관성의 각 호출에 대해 단일 물체를 재사용하기 위해서는, FactoryDefault를 사용할 수 있습니다:

RSpec.describe API::Search, factory_default: :keep do
  let_it_be(:namespace) { create_default(:namespace) }

그럼으로써 우리가 생성하는 모든 프로젝트는 이 namespace를 사용하게 됩니다. 우리가 각 예제에 대해 다시 생성하는 대신에.

우리가 let_it_be와 함께 사용할 수 있도록 만들기 위해서는, factory_default: :keep를 명시적으로 지정해야 합니다. 이것은 각 예제에 대한 기본 팩토리를 다시 생성하는 대신에 유지하여야 한다는 것입니다.

create_default로 생성된 객체들은 의도적인 의존성 사이에서 동결됩니다.

아마도 우리는 208개의 다른 프로젝트를 만들 필요가 없습니다 - 우리는 하나를 만들고 재사용할 수 있을 수 있습니다. 또한, 우리가 요청하는 프로젝트의 약 1/3만이 실제로 만들어졌다는 점을 볼 수 있습니다(76/208). 프로젝트에 대한 기본값을 설정함으로써 이점이 있습니다.

  let_it_be(:project) { create_default(:project) }

이 경우, total timetop-level time 숫자가 더 일치합니다:

   total   top-level     total time      time per call      top-level time               name

      31          30        4.6378s            0.1496s             4.5366s            project
       8           8        0.0477s            0.0477s             0.0477s          namespace
let에 대해 이야기해 봅시다

테스트에서는 여러 가지 방법으로 객체를 생성하고 변수에 저장할 수 있습니다. 효율성이 낮은 방법부터 효율성이 높은 방법까지 다음과 같습니다.

  • let!은 각 예제가 실행되기 전에 객체를 만듭니다. 또한 각 예제마다 새로운 객체를 만듭니다. 명시적으로 참조하지 않고 각 예제가 실행되기 전에 깨끗한 객체를 만들어야 하는 경우에만 이 옵션을 사용해야 합니다.
  • let은 객체를 나중에 생성합니다. 객체가 호출될 때까지는 생성되지 않습니다. let은 일반적으로 모든 예제마다 새로운 객체를 만들기 때문에 비효율적입니다. let은 간단한 값에 대해서는 괜찮지만, 팩토리와 같은 데이터베이스 모델을 다룰 때는 보다 효율적인 변형이 더 좋습니다.
  • let_it_be_with_refindlet_it_be_with_reload와 유사하게 작동하지만 전자는 ActiveRecord::Base#reload 대신에 ActiveRecord::Base#find을 호출합니다. reload가 일반적으로 refind보다 더 빠릅니다.
  • let_it_be_with_reload는 동일한 컨텍스트의 모든 예제에 대해 한 번 객체를 만드는데, 각 예제 이후에 데이터베이스 변경 사항이 롤백되고 object.reload가 호출되어 객체를 원래 상태로 복원합니다. 이는 예제 내 또는 예제 중에 객체를 변경할 수 있음을 의미합니다. 그러나 다른 모델에 상태가 누출될 수 있는 경우가 있습니다. 이러한 경우에는 특히 몇 가지 예제만 있는 경우에는 let이 더 쉬운 옵션이 될 수 있습니다.
  • let_it_be는 동일한 컨텍스트의 모든 예제에 대해 한 번 객체를 만듭니다. 데이터베이스 모델을 생성하는 테스트를 빠르게 할 수 있는 좋은 대안입니다. 자세한 내용 및 예제는 https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be에서 확인하세요.

전문가 팁: 테스트를 작성할 때 let_it_be 내부의 객체를 불변으로 생각하는 것이 가장 좋습니다. 왜냐하면 let_it_be 선언 내부의 객체를 수정할 때 중요한 주의사항이 있기 때문입니다 (1, 2). let_it_be 객체를 불변으로 만들려면 freeze: true를 사용하는 것을 고려해보세요:

# 변경 전
let_it_be(:namespace) { create_default(:namespace) }

# 변경 후
let_it_be(:namespace, freeze: true) { create_default(:namespace) }

let_it_be는 한 번 객체를 인스턴스화하여 예제 전체에서 해당 인스턴스를 공유하기 때문에 가장 최적화된 옵션입니다. let 대신에 let_it_be가 필요한 경우 let_it_be_with_reload을 시도해보세요.

# 변경 전
let(:project) { create(:project) }

# 변경 후
let_it_be(:project) { create(:project) }

# 테스트에서 객체의 변경을 기대해야 하는 경우
let_it_be_with_reload(:project) { create(:project) }

let_it_be를 사용할 수 없지만 let_it_be_with_reload를 사용하면 더 효율적일 수 있는 예제를 확인할 수 있습니다:

let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project) } # `let_it_be`를 사용하면 테스트에 실패함

여기서 `let_it_be` 대신에 `let_it_be_with_reload` 사용하면  효율적일  있는 예제가 있습니다.

'개발자가 있는' 컨텍스트:

project.add_developer(user) 추가하기 전에

'프로젝트에는 소유자와 개발자가 있음' 기대하는데

project.members.map(&:access_level) [Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER] 일치하는지 확인합니다.

'유지자가 있는' 컨텍스트:

project.add_maintainer(user) 추가하기 전에

'프로젝트에는 소유자와 유지자가 있음' 기대하는데

project.members.map(&:access_level) [Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER] 일치하는지 확인합니다.

팩토리 내에서의 메서드 스텁

팩토리에서 allow(object).to receive(:method)를 사용하는 것은 지양해야 합니다. 왜냐하면 이렇게 하면 해당 팩토리를 let_it_be와 함께 사용할 수 없게 되는데, 이에 대해 일반적인 테스트 설정에서 설명되어 있습니다.

대신, 메서드를 스텁하기 위해 stub_method를 사용할 수 있습니다:

  before(:create) do |user, evaluator|
    # 메서드를 스텁합니다.
    stub_method(user, :some_method) { 'stubbed!' }
    # 인수를 포함한 이름이 있는 경우의 메서드 스텁
    stub_method(user, :some_method) { |var1| "Returning #{var1}!" }
    stub_method(user, :some_method) { |var1: 'default'| "Returning #{var1}!" }
  end

  # 메서드를 언스텁합니다.
  # 스텁된 객체가 `let_it_be`로 생성되었고 테스트 간에 메서드를 재설정하려는 경우에 유용합니다.
  after(:create) do  |user, evaluator|
    restore_original_method(user, :some_method)
    # 또는
    restore_original_methods(user)
  end

참고: stub_methodlet_it_be_with_refind와 함께 사용할 수 없습니다. 이것은 stub_method가 인스턴스의 메서드를 스텁하고 let_it_be_with_refind가 각 실행마다 새로운 객체의 인스턴스를 생성하기 때문입니다.

stub_method는 메서드의 존재 및 메서드의 인수를 확인하지 않습니다.

경고: stub_method는 팩토리에서만 사용해야 합니다. 다른 곳에서 사용하는 것은 강력히 권장되지 않습니다. 가능하다면 RSpec 목을 사용하는 것을 고려해보세요.

멤버 액세스 레벨 Stubbing

팩토리 스터브(Project 또는 Group와 같은)의 멤버 액세스 레벨을 스텁하기 위해 stub_member_access_level을 사용하세요:

let(:project) { build_stubbed(:project) }
let(:maintainer) { build_stubbed(:user) }
let(:policy) { ProjectPolicy.new(maintainer, project) }

it 'allows admin_project ability' do
  stub_member_access_level(project, maintainer: maintainer)

  expect(policy).to be_allowed(:admin_project)
end

참고: 테스트 코드가 project_authorizations 또는 Member 레코드를 지속적으로 사용하는 경우에는이 스터브 도우미를 사용하지 마십시오. 대신 Project#add_member 또는 Group#add_member를 사용하세요.

추가 프로파일링 메트릭

rspec_profiling 젬을 사용하여 예를 들어 테스트를 실행할 때 만드는 SQL 쿼리의 수 등을 진단할 수 있습니다.

이는 예를 들어, !123810에서 테스트에 의해 트리거된 응용 프로그램 측 SQL 쿼리에 의해 발생할 수 있습니다.

성능 문서의 지시 사항을 참조하세요.

느린 기능 테스트 문제 해결

느린 기능 테스트는 일반적으로 다른 테스트와 동일한 방식으로 최적화될 수 있습니다. 그러나 문제 해결 세션을 더 유익하게 만들 수 있는 몇 가지 특별한 기술이 있습니다.

사용자 인터페이스에서 기능 테스트의 동작 확인
# 이전
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992

# 이후
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992

더 많은 정보는 가시적 브라우저에서 :js 스펙 실행을 참조하세요.

프로파일링 사용 시 Capybara::DSL# 검색

stackprof 플레임그래프를 사용할 때 Capybara::DSL#을 검색하여 만들어진 capybara 작업 및 소요 시간을 확인하세요!

느린 테스트 식별

프로파일링으로 특정 테스트를 최적화하는 좋은 방법입니다. 이를 다음과 같이 수행할 수 있습니다:

bundle exec rspec --profile -- path/to/spec_file.rb

다음과 같은 정보를 포함합니다:

Top 10 slowest examples (10.69 seconds, 7.7% of total time):
  Issue behaves like an editable mentionable creates new cross-reference notes when the mentionable text is edited
    1.62 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:164
  Issue relative positioning behaves like a class that supports relative positioning .move_nulls_to_end manages to move nulls to the end, stacking if we cannot create enough space
    1.39 seconds ./spec/support/shared_examples/models/relative_positioning_shared_examples.rb:88
  Issue relative positioning behaves like a class that supports relative positioning .move_nulls_to_start manages to move nulls to the end, stacking if we cannot create enough space
    1.27 seconds ./spec/support/shared_examples/models/relative_positioning_shared_examples.rb:180
  Issue behaves like an editable mentionable behaves like a mentionable extracts references from its reference property
    0.99253 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:69
  Issue behaves like an editable mentionable behaves like a mentionable creates cross-reference notes
    0.94987 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:101
  Issue behaves like an editable mentionable behaves like a mentionable when there are cached markdown fields sends in cached markdown fields when appropriate
    0.94148 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:86
  Issue behaves like an editable mentionable when there are cached markdown fields when the markdown cache is stale persists the refreshed cache so that it does not have to be refreshed every time
    0.92833 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:153
  Issue behaves like an editable mentionable when there are cached markdown fields refreshes markdown cache if necessary
    0.88153 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:130
  Issue behaves like an editable mentionable behaves like a mentionable generates a descriptive back-reference
    0.86914 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:65
  Issue#related_issues returns only authorized related issues for given user
    0.84242 seconds ./spec/models/issue_spec.rb:335

2분 19초 소요 (파일 로드에 1분 4.42초 소요)
277개의 예제, 실패 0개, 보류 1개

이 결과에서 가장 시간이 많이 소요되는 예제를 찾아 시작 점을 찾을 수 있습니다. 이러한 예제들은 공유된 예제에서 호출되므로 줄이면 더 큰 영향을 미칩니다.

최상위 느린 테스트

rspec_profiling_stats 프로젝트에서 테스트 기간에 대한 정보를 수집합니다. 이 데이터는 UI에서 GitLab Pages를 사용하여 표시됩니다.

이슈를 통해 가이드 역할을 할 수 있는 테스트 기간의 임계값을 정의했습니다.

임계값을 충족하지 못하는 테스트에 대해 개선하기 위해 자동으로 이슈를 작성합니다.

합법적인 이유로 느린 테스트이며 이슈를 생성하지 않으려면 allowed_to_be_slow: true를 추가하세요.

날짜 피처 테스트 컨트롤러 및 요청 테스트 유닛 다른 메소드
2023-02-15 67.42 초 44.66 초 - 76.86 초 최상위 느린 테스트는 최대로 제거
2023-06-15 50.13 초 19.20 초 27.12 45.40 초 상위 100개 느린 테스트 평균

비싼 작업 반복 피하기

개별적인 예제는 매우 명확하며 명세의 목적을 제공하는 반면, 아래 예제는 비싼 작업을 결합하는 방법을 보여줍니다:

subject { described_class.new(arg_0, arg_1) }

it '이벤트를 생성합니다' do
  expect { subject.execute }.to change(Event, :count).by(1)
end

it '프로뷸렌스를 설정합니다' do
  expect { subject.execute }.to change { arg_0.reset.frobulance }.to('wibble')
  
end

it '백그라운드 작업을 예약합니다' do
  expect(BackgroundJob).to receive(:perform_async)

  subject.execute
end

만약 subject.execute 호출이 비싸다면, 우리는 다른 단언을 하기 위해 동일한 작업을 반복하게 됩니다. 우리는 이러한 반복을 최소화하기 위해 예제를 결합할 수 있습니다:

it '예상된 부작용을 수행합니다' do
  expect(BackgroundJob).to receive(:perform_async)

  expect { subject.execute }
    .to change(Event, :count).by(1)
    .and change { arg_0.frobulance }.to('wibble')
end

성능 향상을 위해 명확성과 테스트 독립성을 희생시키므로 주의하십시오.

테스트를 결합할 때, 전체 결과가 사용 가능하고 첫 번째 실패뿐만 아니라 전체 결과가 사용 가능하도록 :aggregate_failures를 사용하는 것을 고려하세요.

막혔을 때

느린 백엔드 스펙을 리팩토링하는 데 도움을 줄 수 있는 backend_testing_performance 도메인 전문 지식을 찾아보세요.

도움을 줄 수 있는 사람을 찾으려면 Engineering Projects 페이지에서 backend testing performance를 검색하거나 www-gitlab-org 프로젝트에서 직접 찾아보세요.

피처 카테고리 메타데이터

각 RSpec 예제에 피처 카테고리 메타데이터를 설정해야 합니다.

EE 라이선스에 따른 테스트

FOSS_ONLY=1로 실행 여부에 따라 테스트를 실행하려면 컨텍스트/스펙 블록에 if: Gitlab.ee? 또는 unless: Gitlab.ee?를 사용할 수 있습니다.

예: SchemaValidator는 라이선스에 따라 다른 경로를 읽습니다

SaaS에 따른 테스트

컨텍스트/스펙 블록에서 :saas RSpec 메타데이터 태그 도우미를 사용하여 GitLab.com에서만 실행되는 코드를 테스트할 수 있습니다. 이 도우미는 Gitlab.config.gitlab['url']Gitlab::Saas.com_url로 설정합니다.

커버리지

코드 테스트 커버리지 보고서를 생성하는 데 simplecov가 사용됩니다. 이들은 CI에서 자동으로 생성되지만 로컬에서 테스트를 실행할 때 일부 보고서를 생성하기 위해서 SIMPLECOV 환경 변수를 설정하세요:

SIMPLECOV=1 bundle exec rspec spec/models/repository_spec.rb

커버리지 보고서는 앱 루트의 coverage 폴더에 생성되며 브라우저에서 열 수 있습니다. 예를 들어:

firefox coverage/index.html

커버리지 보고서를 사용하여 테스트가 코드의 100%를 커버하도록 합니다.

시스템 / 기능 테스트

참고: 새로운 시스템 테스트를 작성하기 전에, 시스템 테스트를 작성하지 않는 것을 고려해 보세요!

  • 기능 스펙은 ROLE_ACTION_spec.rb와 같이 명명되어야 합니다. 예를 들어 user_changes_password_spec.rb.
  • 성공 및 실패 경로를 설명하는 시나리오 제목을 사용하세요.
  • “successfully”와 같이 어떤 정보도 추가하지 않는 시나리오 제목은 피하세요.
  • 기능 제목을 반복하는 시나리오 제목은 피하세요.
  • 데이터베이스에는 필요한 레코드만 생성하세요.
  • 행복한 경로와 적은 기쁨 경로를 테스트하세요.
  • 나머지 가능한 경로는 Unit 또는 Integration 테스트로 테스트하세요.
  • ActiveRecord 모델의 내부가 아닌 페이지에 표시된 내용을 테스트하세요. 예를 들어, 레코드가 생성되었음을 확인하려면 속성이 페이지에 표시되는지 확인하는 기대를 추가하세요. Model.count가 1 증가했다는 기대는 추가하지 마세요.
  • DOM 요소를 찾는 것은 괜찮지만, 테스트를 더 취약하게 만들기 때문에 남용하지 마세요.

UI 테스트

UI를 테스트할 때, 사용자가 보는 것과 UI와 상호 작용하는 방식을 모방하는 테스트를 작성하세요. 이것은 Capybara의 의미론적인 메소드를 선호하고 ID, 클래스 또는 속성으로 쿼리하는 것은 피하는 것을 의미합니다.

이 방식으로 테스트하는 이점은 다음과 같습니다.

  • 모든 상호 작용 요소에 접근 가능한 이름이 있는지 보장합니다.
  • 보다 자연스러운 언어를 사용하므로 가독성이 높아집니다.
  • 사용자에게 보이지 않는 ID, 클래스, 및 속성으로 쿼리하는 것을 피하기 때문에 덜 취약해집니다.

가능하다면, ID, 클래스 이름 또는 data-testid 대신 요소의 텍스트 레이블로 쿼리하는 것을 권장합니다.

필요하다면, within을 사용하여 페이지의 특정 영역 내에서 상호작용을 범위로 지정할 수 있습니다. 일반적으로 레이블이 없는 요소(예: div)를 범위로 지정할 가능성이 높기 때문에 이 경우에는 data-testid 셀렉터를 사용할 수 있습니다.

be_axe_clean 매처를 사용하여 axe 자동 접근성 테스트를 feature 테스트에서 실행할 수 있습니다.

외부화된 내용

RSpec 테스트에서 외부화된 내용에 대한 기대는 동일한 외부화 메소드를 호출해야 합니다. 예를 들어, Ruby에서 _ 메소드를 사용해야 합니다.

자세한 내용은 GitLab을 위한 국제화 - 테스트 파일 (RSpec)을 참조하세요.

행위

가능한 경우, 아래와 같이 더 구체적인 행위를 사용하세요.

# 좋은 예
click_button _('Submit review')

click_link _('UI testing docs')

fill_in _('Search projects'), with: 'gitlab' # 텍스트 입력란에 텍스트를 입력합니다

select _('Updated date'), from: 'Sort by' # 셀렉트 입력에서 옵션을 선택합니다

check _('Checkbox label')
uncheck _('Checkbox label')

choose _('Radio input label')

attach_file(_('Attach a file'), '/path/to/file.png')

# 나쁜 예 - 인터랙티브 요소는 접근 가능한 이름이 있어야하므로
# 위의 특정 행위 중 하나를 사용할 수 있어야 합니다.
find('.group-name', text: group.name).click
find('.js-show-diff-settings').click
find('[data-testid="submit-review"]').click
find('input[type="checkbox"]').click
find('.search').native.send_keys('gitlab')
파인더

가능한 경우, 아래와 같이 더 구체적인 파인더를 사용하세요.

# 좋은 예
find_button _('Submit review')
find_button _('Submit review'), disabled: true

find_link _('UI testing docs')
find_link _('UI testing docs'), href: docs_url

find_field _('Search projects')
find_field _('Search projects'), with: 'gitlab' # 텍스트를 포함하는 입력란을 찾습니다
find_field _('Search projects'), disabled: true
find_field _('Checkbox label'), checked: true
find_field _('Checkbox label'), unchecked: true

# 버튼, 링크 또는 필드가 아닌 요소를 찾을 때는 허용됩니다
find('[data-testid="element"]')
매처

가능한 경우, 아래와 같이 더 구체적인 매처를 사용하세요.

# 좋은 예
expect(page).to have_button _('Submit review')
expect(page).to have_button _('Submit review'), disabled: true
expect(page).to have_button _('Notifications'), class: 'is-checked' # "Notifications" GlToggle이 선택되었음을 단언합니다

expect(page).to have_link _('UI testing docs')
expect(page).to have_link _('UI testing docs'), href: docs_url # 링크가 href를 가지고 있는지 단언합니다

expect(page).to have_field _('Search projects')
expect(page).to have_field _('Search projects'), disabled: true
expect(page).to have_field _('Search projects'), with: 'gitlab' # 입력란에 텍스트가 있는지 단언합니다

expect(page).to have_checked_field _('Checkbox label')
expect(page).to have_unchecked_field _('Radio input label')

expect(page).to have_select _('Sort by')
expect(page).to have_select _('Sort by'), selected: 'Updated date' # 옵션이 선택되었는지 단언합니다
expect(page).to have_select _('Sort by'), options: ['Updated date', 'Created date', 'Due date'] # 정확한 옵션 목록을 단언합니다
expect(page).to have_select _('Sort by'), with_options: ['Created date', 'Due date'] # 일부 옵션 목록을 단언합니다

expect(page).to have_text _('Some paragraph text.')
expect(page).to have_text _('Some paragraph text.'), exact: true # 정확한 일치를 단언합니다

expect(page).to have_current_path 'gitlab/gitlab-test/-/issues'

expect(page).to have_title _('Not Found')

# 상세 매처를 사용할 수 없을 때는 허용됩니다
expect(page).to have_css 'h2', text: 'Issue title'
expect(page).to have_css 'p', text: 'Issue description', exact: true
expect(page).to have_css '[data-testid="weight"]', text: 2
expect(page).to have_css '.atwho-view ul', visible: true
모달 대화 상자와 상호 작용

within_modal 도우미를 사용하여 GitLab UI 모달과 상호 작용하세요.

include Spec::Support::Helpers::ModalHelpers

within_modal do
  expect(page).to have_link _('UI testing docs')

  fill_in _('Search projects'), with: 'gitlab'

  click_button 'Continue'
end

더 나아가, 확인이 필요한 확인 대화 상자에 대해서는 accept_gl_confirm을 사용할 수 있습니다. 이것은 window.confirm()confirmAction으로 마이그레이션할 때 유용합니다.

include Spec::Support::Helpers::ModalHelpers

accept_gl_confirm do
  click_button 'Delete user'
end

또한, accept_gl_confirm에 예상 확인 메시지와 버튼 텍스트를 전달할 수 있습니다.

include Spec::Support::Helpers::ModalHelpers

accept_gl_confirm('이 사용자를 삭제하시겠습니까?', button_text: '삭제') do
  click_button 'Delete user'
end
유용한 기타 메서드

파인더 메서드를 사용하여 요소를 검색한 후, hover와 같은 여러 요소 메서드를 호출할 수 있습니다.

또한 Capybara 테스트에는 accept_confirm과 같은 여러 세션 메서드가 있습니다.

아래에 다른 유용한 메서드가 표시됩니다:

refresh # 페이지 새로 고침

send_keys([:shift, 'i']) # Shift+I 키를 눌러 이슈 대시보드 페이지로 이동

current_window.resize_to(1000, 1000) # 창 크기 조정

scroll_to(find_field('Comment')) # 요소로 스크롤

또한, spec/support/helpers/ 디렉터리에서 여러 GitLab 사용자 지정 도우미를 찾을 수 있습니다.

실시간 디버깅

가끔은 브라우저 동작을 관찰하면서 Capybara 테스트를 디버깅해야 할 수도 있습니다.

live_debug 메서드를 사용하여 테스트 중에 Capybara를 일시 중지하고 브라우저에서 웹사이트를 볼 수 있습니다. 현재 페이지가 자동으로 기본 브라우저에서 열립니다. 테스트 실행을 계속하려면 아무 키나 누르세요.

예를 들어:

$ bin/rspec spec/features/auto_deploy_spec.rb:34
Running via Spring preloader in process 8999
Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}

현재 예제가 실시간 디버깅을 위해 일시 중지되었습니다
현재 사용자 자격 증명은: user2 / 12345678
테스트 실행을 계속하려면 아무 키나 누르세요!
예제로 돌아갑니다!
.

34.51초 내에 완료(파일로드에 0.76702초 걸림)
1 예제, 0 실패

live_debug는 JavaScript가 활성화된 특정 사양에서만 작동합니다.

가시적 브라우저에서 :js 사양 실행

다음과 같이 WEBDRIVER_HEADLESS=0과 함께 사양을 실행하세요.

WEBDRIVER_HEADLESS=0 bin/rspec some_spec.rb

테스트는 빠르게 완료됩니다. 하지만 이것은 무엇이 발생하는지에 대한 아이디어를 제공합니다. WEBDRIVER_HEADLESS=0와 함께 live_debug를 사용하면 열려 있는 브라우저를 일시 중지하고 페이지를 다시 열지 않습니다. 이를 사용하여 디버깅하고 요소를 검사할 수 있습니다.

또한 실행을 중지하고 단계별로 테스트를 진행할 수 있도록 byebug 또는 binding.pry를 추가할 수도 있습니다.

스크린샷

우리는 capybara-screenshot 젬을 사용하여 실패할 경우 자동으로 스크린샷을 찍습니다. CI에서는 작업 artifacts로 이 파일을 다운로드할 수 있습니다.

또한 다음과 같은 방법으로 테스트 중에 수동으로 스크린샷을 찍을 수도 있습니다. 더 이상 필요하지 않을 때는 꼭 제거하세요! 더 많은 정보는 https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots에서 확인하세요.

:js 사양에 screenshot_and_save_page를 추가하여 Capybara가 “보는” 것을 스크린샷으로 찍고 페이지 소스를 저장하세요.

:js 사양에 screenshot_and_open_image를 추가하여 Capybara가 “보는” 것을 스크린샷으로 찍고 이미지를 자동으로 열어보세요.

이로 생성된 HTML 덤프에는 CSS가 누락됩니다. 이로 인해 실제 응용 프로그램과 매우 다르게 보입니다. 디버깅을 쉽게 만드는 CSS를 추가하는 작은 해킹이 있습니다.

빠른 단위 테스트

일부 클래스는 Rails에서 완전히 격리되어 있습니다. 이러한 경우에는 테스트 파일에서 require 'spec_helper' 대신에 require 'fast_spec_helper'를 사용하여 테스트를 실행하고 테스트가 빠르게 실행되어야 합니다. 그 이유는 다음과 같습니다:

  • 젬 로딩이 건너뛰어집니다.
  • Rails 앱 부트가 건너뛰어집니다.
  • GitLab Shell 및 Gitaly 설정이 건너뛰어집니다.
  • 테스트 저장소 설정이 건너뛰어집니다.

또한, fast_spec_helperlib/ 디렉토리 내부에 있는 클래스를 자동으로 로드하는 기능도 지원합니다. 따라서 당신의 클래스나 모듈이 lib/ 디렉토리에서의 코드만을 사용하는 경우에는 명시적으로 어떤 종속성도 로드할 필요가 없습니다. fast_spec_helper는 Rails 환경에서 일반적으로 사용되는 핵심 확장을 포함하여 모든 ActiveSupport 확장을 로드합니다.

일부 경우에는 코드가 젬을 사용하거나 종속성이 lib/에 위치해 있지 않은 경우에는 여전히 require_dependency를 사용하여 일부 종속성을 로드해야 할 수 있습니다. 예를 들어, 정보 검색시 Gitlab::UntrustedRegexp 클래스를 호출하는 코드를 테스트하려는 경우에는 re2 라이브러리를 사용하므로 다음을 참조하세요:

  • re2 젬이 필요한 라이브러리 파일에 require_dependency 're2'를 추가하여 이 요구 사항을 명시적으로 만듭니다. 이 방법이 우선됩니다.
  • 테스트 파일 자체에 추가합니다.
  • RuboCop 관련 스펙에는 rubocop_spec_helper를 사용하세요.

fast_spec_helper를 사용하면 일반 spec_helper의 경우 30초 이상이 걸리는 테스트 로딩 시간이 1초 정도로 단축됩니다.

경고: 코드 및 해당 스펙이 Rails로부터 충분히 격리되어 있는지 확인하려면 bin/rspec을 통해 스펙을 개별적으로 실행하세요. bin/spring rspec는 자동으로 spec_helper를 로드하므로 사용하지 마세요.

subjectlet 변수

GitLab RSpec 스위트는 중복을 줄이기 위해 let (엄격하고 레이지하지 않은 버전인 let!와 함께) 변수를 광범위하게 활용했습니다. 그러나 이는 때로 명확성을 희생시킬 수 있으므로 앞으로 사용에 대한 일부 지침을 설정해야 합니다:

  • let! 변수는 인스턴스 변수보다 선호됩니다. let 변수는 let! 변수보다 선호됩니다. 로컬 변수는 let 변수보다 선호됩니다.
  • 전체 스펙 파일 전체에 걸쳐 중복을 줄이기 위해 let을 사용하세요.
  • 하나의 테스트에서 사용되는 단일 변수를 정의하기 위해 let을 사용하지 마세요. 해당 변수는 테스트의 it 블록 내부의 로컬 변수로 정의하세요.
  • describe 블록의 최상위에서보다는 보다 깊이 중첩된 context 또는 describe 블록에서만 사용되는 let 변수를 정의하지 마세요. 최대한 사용되는 곳에 가깝게 정의하세요.
  • 하나의 let 변수의 정의를 다른 것으로 덮어쓰는 것을 피하세요.
  • 다른 정의에만 사용되는 let 변수를 정의하지 마세요. 대신 도우미 메서드를 사용하세요.
  • 엄격한 평가가 순서대로 필요한 경우에만 let! 변수를 사용하세요. 그렇지 않으면 let으로 충분합니다. let이 레이지하므로 참조될 때까지 평가되지 않습니다.
  • 예제에서 subject를 참조하지 않도록 하세요. 명명된 subject subject(:name) 또는 let 변수를 대신 사용하여 변수에 컨텍스트 이름을 부여하세요.
  • subject가 예제 내에서 참조되지 않는 경우, 이름을 가진 subject를 정의하는 것이 허용됩니다.

공통 테스트 설정

참고: let_it_bebefore_all은 DatabaseCleaner의 삭제 전략과 호환되지 않습니다. 이에는 마이그레이션 스펙, Rake 작업 스펙 및 :delete RSpec 메타데이터 태그가있는 스펙이 포함됩니다. 자세한 내용은 이슈 420379를 참조하세요.

일부 경우에는 같은 개체를 테스트를 위해 각 예제마다 다시 만들 필요가 없을 수 있습니다. 예를 들어, 프로젝트와 해당 프로젝트의 게스트를 테스트하기 위해 동일한 프로젝트와 사용자가있는 경우, 전체 파일에 대해 하나의 프로젝트와 사용자가 충분합니다.

가능한 한 before(:all) 또는 before(:context)을 사용하여이를 구현하지 마세요. 그렇게하면 이러한 훅은 데이터베이스 트랜잭션 외부에서 실행되므로 데이터를 수동으로 정리해야 합니다.

대신에, 이는 test-proflet_it_be 변수와 before_all 훅을 사용하여 달성할 수 있습니다.

let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }

before_all do
  project.add_guest(user)
end

이 결과는이 컨텍스트에 대해 생성된 단일 Project, UserProjectMember만 있습니다.

let_it_bebefore_all은 중첩된 컨텍스트에서도 사용할 수 있습니다. 컨텍스트 이후에 정리는 자동으로 트랜잭션 롤백을 사용하여 처리됩니다.

let_it_be 블록 내에서 정의된 개체를 수정하는 경우 다음 중 하나를 수행해야 합니다.

  • 필요에 따라 객체를 다시로드합니다.
  • let_it_be_with_reload 별칭을 사용하세요.
  • 모든 예제에 대해 다시로드하도록 reload 옵션을 지정하세요.

let_it_be_with_refind 별칭을 사용하거나 refind 옵션을 명시하여 완전히 새로운 개체를 완전히 로드할 수도 있습니다.

let_it_beallow와 같은 스텁이있는 팩토리와 사용할 수 없습니다. 그 이유는 let_it_bebefore(:all) 블록에서 발생하며 RSpec이 before(:all)에서 스텁을 허용하지 않기 때문입니다. 자세한 내용은 이슈를 참조하세요. 해결하려면 let을 사용하거나 팩토리를 스텁을 사용하지 않도록 변경하세요.

시간에 민감한 테스트

ActiveSupport::Testing::TimeHelpers를 사용하여 시간에 민감한 사항을 확인할 수 있습니다. 시간에 민감한 사항을 연습하거나 확인하는 모든 테스트는 일시적인 테스트 실패를 방지하기 위해 이러한 도우미를 사용해야 합니다.

예시:

it '만료되었는지 확인' do
  issue = build(:issue, due_date: Date.tomorrow)

  travel_to(3.days.from_now) do
    expect(issue).to be_overdue
  end
end

RSpec 도우미

ActiveSupport::Testing::TimeHelpers 메소드를 전체 스펙을 래핑하는 데 필요한 보일러플레이트 코드의 양을 줄이는 데 도움이 되는 :freeze_time:time_travel_to RSpec 메타데이터 태그 도우미를 사용할 수 있습니다.

describe '시간이 얼려져야 하는 스펙', :freeze_time do
  it '시간을 얼립니다' do
    right_now = Time.now

    expect(Time.now).to eq(right_now)
  end
end

describe '특정한 날짜와/또는 시간으로 시간이 얼려져야 하는 스펙', time_travel_to: '2020-02-02 10:30:45 -0700' do
  it '지정한 날짜와 시간으로 시간을 얼립니다' do
    expect(Time.now).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
  end
end

내부 구현에서 이러한 도우미는 around(:each) 훅과 ActiveSupport::Testing::TimeHelpers 메소드의 블록 구문을 사용합니다.

around(:each) do |example|
  freeze_time { example.run }
end

around(:each) do |example|
  travel_to(date_or_time) { example.run }
end

예제가 실행되기 전에 만들어진 개체(예: let_it_be를 통해 만들어진 개체)는 스펙 범위를 벗어날 것이라는 것을 기억하세요. 모든 것의 시간을 얼려야 하는 경우에는 before :all을 사용하여 설정을 캡슐화할 수 있습니다.

before :all do
  freeze_time
end

after :all do
  unfreeze_time
end

타임스탬프 절삭

Active Record 타임스탬프는 Rails의 ActiveRecord::Timestamp 모듈을 사용하여 설정되며, Time.now를 사용합니다. 시간 정밀도는 운영 체제에 따라 다르며, 문서에 명시된 대로 소수점 이하의 숫자를 포함할 수 있습니다.

Rails 모델이 데이터베이스에 저장될 때, 가지고 있는 모든 타임스탬프는 timestamp without time zone 타입을 사용하여 PostgreSQL에 저장됩니다. 이는 마이크로초 해상도인데, 즉 소수점 아래에 여섯 자리 숫자를 포함합니다. 그래서 1577987974.6472975가 PostgreSQL에 전송되면, 마지막 소수점 이하 자리가 절삭되어 1577987974.647297로 저장됩니다.

이로 인해 단순한 테스트에서 다음과 같은 오류가 발생할 수 있습니다.

let_it_be(:contact) { create(:contact) }

data = Gitlab::HookData::IssueBuilder.new(issue).build

expect(data).to include('customer_relations_contacts' => [contact.hook_attrs])

아래와 유사한 오류가 발생할 수 있습니다.

expected {
"assignee_id" => nil, "...1 +0000 } to include {"customer_relations_contacts" => [{:created_at => "2023-08-04T13:30:20Z", :first_name => "Sidney Jones3" }]}

Diff:
       @@ -1,35 +1,69 @@
       -"customer_relations_contacts" => [{:created_at=>"2023-08-04T13:30:20Z", :first_name=>"Sidney Jones3" }],
       +"customer_relations_contacts" => [{"created_at"=>2023-08-04 13:30:20.245964000 +0000, "first_name"=>"Sidney Jones3" }],

올바른 정밀도의 타임스탬프를 얻기 위해 데이터베이스에서 객체를 .reload하는 것이 해결책입니다.

let_it_be(:contact) { create(:contact) }

data = Gitlab::HookData::IssueBuilder.new(issue).build

expect(data).to include('customer_relations_contacts' => [contact.reload.hook_attrs])

이 설명은 Maciek Rząsa의 블로그 글에서 가져왔습니다.

이 문제가 발생한 병합 요청과 이에 대한 백앤드 페어링 세션을 확인할 수 있습니다.

테스트에서의 기능 플래그

이 섹션은 기능 플래그로 개발으로 이동되었습니다.

테스트 환경의 초기화

단일 GitLab 테스트에서 실행되는 코드는 여러 데이터를 액세스하고 수정할 수 있습니다. 테스트가 실행되기 전에 신중한 준비와 이후의 정리 없이 데이터가 변경되면 후속 테스트의 동작에 영향을 미칠 수 있습니다. 이를 최대한 피해야 합니다! 다행히도 기존의 테스트 프레임워크는 대부분의 경우를 이미 처리합니다.

테스트 환경이 오염되면 일반적인 결과로 불안정한 테스트가 됩니다. 오염은 종종 순서 종속성으로 나타납니다: spec A를 실행한 후에 spec B를 실행하면 항상 실패하지만, spec B를 실행한 후에 spec A를 실행하면 항상 성공합니다. 이러한 경우에는 rspec --bisect를 사용하거나(또는 수동으로 spec 파일을 이분 검색) 어떤 spec이 잘못되었는지 확인할 수 있습니다. 문제를 해결하려면 테스트 스위트가 환경이 초기화됨을 어떻게 보장하는지에 대한 이해가 필요합니다. 각 데이터 저장소에 대해 더 알아보려면 계속 읽어보세요!

SQL 데이터베이스

이 작업은 database_cleaner 젬이 우리를 위해 관리합니다. 각 spec은 트랜잭션으로 둘러싸여 있으며, 테스트가 완료된 후에 롤백됩니다. 일부 spec은 완료 후에 모든 테이블에 대해 DELETE FROM 쿼리를 발행합니다. 이를 통해 생성된 행을 여러 데이터베이스 연결에서 볼 수 있으므로 브라우저에서 실행되는 spec이나 마이그레이션 spec 등에 중요합니다.

이러한 전략을 사용하는 한 가지 결과는 널리 알려진 TRUNCATE TABLES 접근 방식 대신 primary key 및 기타 시퀀스가 spec 간에 재설정되지 않는다는 것입니다. 따라서 spec A에서 프로젝트를 생성한 다음 spec B에서 프로젝트를 생성하면 첫 번째는 id=1이고 두 번째는 id=2입니다.

즉, spec는 ID 값 또는 다른 시퀀스 생성 열의 값을 의존해서는 안되며, 이러한 종류의 열에 대한 값은 수동으로 지정하지 않아야 합니다. 대신에 지정하지 말고, 행이 생성된 후에 값으로 조회해야 합니다.

마이그레이션 spec에서의 TestProf

위에서 설명한 바와 같이, 마이그레이션 spec는 데이터베이스 트랜잭션 내에서 실행할 수 없습니다. 저희 테스트 스위트는 테스트 스위트의 실행 시간을 개선하기 위해 TestProf를 사용하지만, TestProf는 이러한 최적화를 수행하기 위해 데이터베이스 트랜잭션을 사용합니다. 이러한 이유로 우리는 마이그레이션 spec에서 TestProf 메서드를 사용할 수 없습니다. 다음은 사용되어서는 안되며 대신 기본 RSpec 메서드로 대체해야 하는 메서드입니다:

  • let_it_be: 대신에 let 또는 let!을 사용하세요.
  • let_it_be_with_reload: 대신에 let 또는 let!을 사용하세요.
  • let_it_be_with_refind: 대신에 let 또는 let!을 사용하세요.
  • before_all: 대신에 before 또는 before(:all)을 사용하세요.

Redis

GitLab은 Redis에 두 가지 주요 데이터 (캐시된 항목 및 Sidekiq 작업)을 저장합니다. 별도의 Redis 인스턴스에서 지원되는 Gitlab::Redis::Wrapper 하위 항목의 전체 목록을 확인할 수 있습니다.

대부분의 spec에서 Rails 캐시는 사실상 메모리 내 저장소입니다. 이는 spec 사이에 교체되므로 Rails.cache.readRails.cache.write 호출은 안전합니다. 그러나 spec이 직접적으로 Redis 호출을 하면 적절한 :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state 또는 :clean_gitlab_redis_queues 특성으로 자신을 표시해야 합니다.

Background jobs / Sidekiq

기본적으로 Sidekiq 작업은 작업 배열에 인큐되어 처리되지 않습니다. 테스트가 Sidekiq 작업을 대기열에 집어넣고 처리해야 할 경우 :sidekiq_inline 특성을 사용할 수 있습니다.

Sidekiq 인라인 모드가 가짜 모드로 변경되었을 때, :sidekiq_might_not_need_inline 특성은 실제로 작업을 처리해야 하는 모든 테스트에 추가되었습니다. 이 특성이 있는 테스트는 Sidekiq 작업 처리에 의존하지 않도록 수정하거나 :sidekiq_might_not_need_inline 특성을 작업 처리가 필요한/예상되는 경우 :sidekiq_inline로 업데이트해야 합니다.

perform_enqueued_jobs의 사용은 지연된 메일 전송을 테스트하는 경우에만 유용합니다. 왜냐하면 우리의 Sidekiq 워커들은 ApplicationJob/ActiveJob::Base에서 상속되지 않기 때문입니다.

DNS

DNS 요청은 테스트 스위트 전반에서 스텁 처리되었습니다 (!22368를 기준으로), 로컬 네트워크에 따라 DNS가 문제를 일으킬 수 있기 때문입니다. 필요한 경우 테스트에 적용할 수 있는 spec/support/dns.rb에 있는 RSpec 라벨이 있습니다.

it "really connects to Prometheus", :permit_dns do

그리고 더 구체적인 제어가 필요한 경우에는 DNS 차단이 spec/support/helpers/dns_helpers.rb에 구현되었으며 다른 곳에서 이러한 메서드를 호출할 수 있습니다.

속도 제한

테스트 스위트에서 속도 제한이 활성화되었습니다. 재한 사항은 :js 특성을 사용하는 기능 스펙에서 트리거될 수 있습니다. 대부분의 경우, 트리거하게 되는 속도 제한은 :clean_gitlab_redis_rate_limiting 특성으로 스펙을 표시함으로써 피할 수 있습니다. 이 특성은 스펙 간에 Redis 캐시에 저장된 속도 제한 데이터를 지웁니다. 특정 테스트가 속도 제한을 트리거한다면 대신 :disable_rate_limit를 사용할 수 있습니다.

파일 메서드의 스터빙

파일 내용을 스터브해야 하는 상황에서 stub_file_readexpect_file_read 도우미 메서드를 사용하면 File.read의 스터빙을 올바르게 처리합니다. 이러한 메서드들은 특정 파일 이름에 대해 File.read를 스터빙하고 File.exist?true를 반환하도록 스터빙합니다.

어떤 이유로든 수동으로 File.read를 스터빙해야한다면 다음을 반드시 지켜야 합니다:

  1. 다른 파일 경로에 대해 스터브를 만들고 원래 구현을 호출합니다.
  2. 그런 다음 File.read를 관심 있는 파일 경로에 대해서만 스터빙합니다.

그렇지 않으면 코드베이스의 다른 부분에서 File.read 호출이 잘못 스터빙됩니다.

# 나쁨, 모든 파일이 읽혀지고 아무 값도 반환되지 않음
allow(File).to receive(:read)

# 좋음
stub_file_read(my_filepath, content: "가짜 파일 내용")

# 또한 좋음
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(my_filepath).and_return("가짜 파일 내용")

파일 시스템

파일 시스템 데이터는 대략 “저장소”와 “그 외 모든 것”으로 분할됩니다. 저장소는 tmp/tests/repositories에 저장됩니다. 이 디렉토리는 테스트 실행 전에 비워지고, 테스트 실행이 끝난 후에도 비워집니다. 그러나 스펙 간에는 비워지지 않으므로 생성된 저장소는 프로세스의 수명 동안 이 디렉토리에 누적됩니다. 이들을 삭제하는 것은 비용이 많이 듭니다. 신중하게 관리되지 않으면 오염될 수 있습니다.

이를 피하기 위해 해시된 저장소가 테스트 스위트에서 활성화됩니다. 이는 저장소에 프로젝트 ID에 따라 고유한 경로가 지정된다는 것을 의미합니다. 프로젝트 ID가 스펙 간에 재설정되지 않으므로 각 스펙은 자체 디스크에 저장소를 갖게되며, 스펙 간에 변경 사항이 보이는 것을 방지합니다.

특정 스펙이 프로젝트 ID를 수동으로 지정하거나 tmp/tests/repositories/ 디렉터리의 상태를 직접 검사하는 경우, 해당 스펙은 실행 전후에 디렉터리를 정리해야 합니다. 일반적으로 이러한 패턴은 완전히 피해야 합니다.

업로드와 같이 데이터베이스 객체에 연결된 다른 종류의 파일은 일반적으로 동일한 방식으로 관리됩니다. 스펙에서 해시된 저장소가 활성화되면 ID에 따라 디스크에 기록되므로 충돌이 발생하지 않아야 합니다.

해시된 저장소를 비활성화하는 일부 스펙은 projects 팩토리에 :legacy_storage 특성을 전달하여 수행합니다. 이렇게 하는 스펙은 결코 프로젝트의 path 또는 해당 그룹 중 하나를 재정의해서는 안됩니다. 기본 경로에는 프로젝트 ID가 포함되어 있기 때문에 충돌이 발생하지 않습니다. 두 스펙이 같은 경로의 :legacy_storage 프로젝트를 생성하는 경우, 동일한 디스크의 저장소를 사용하게 되어 테스트 환경이 오염됩니다.

다른 파일은 스펙에 의해 수동으로 관리되어야 합니다. 예를 들어 tmp/test-file.csv 파일을 생성하는 코드를 실행하는 경우, 해당 스펙은 파일이 정리될 수 있도록 해야합니다.

영구적인 메모리 내 응용 프로그램 상태

주어진 rspec 실행에서 모든 스펙은 동일한 Ruby 프로세스를 공유하기 때문에 서로 영향을 미칠 수 있습니다. 이는 전역 변수 및 상수(예: Ruby 클래스, 모듈 등)를 수정함으로써 발생할 수 있습니다.

일반적으로 전역 변수는 수정되면 안됩니다. 만약 절대적으로 필요한 경우에는 다음과 같은 블록을 사용하여 변경 사항이 이후에 롤백되도록 할 수 있습니다:

around(:each) do |example|
  old_value = $0

  begin
    $0 = "new-value"
    example.run
  ensure
    $0 = old_value
  end
end

스펙에서 상수를 수정해야 하는 경우, 변경 사항이 롤백되도록 stub_const 도우미를 사용해야 합니다.

ENV 상수의 내용을 수정해야 하는 경우 stub_env 도우미 메서드를 사용할 수 있습니다.

대부분의 Ruby 인스턴스는 스펙 간에 공유되지 않지만 클래스모듈은 일반적으로 공유됩니다. 클래스 및 모듈 인스턴스 변수, 엑세서, 클래스 변수 및 기타 상태 관련된 방식은 전역 변수와 동일하게 처리해야 합니다. 필요한 경우 기대값 또는 의존성 주입과 스터브와 함께 사용하여 수정이 필요한 경우에는 사용을 피해야 합니다. 절대적으로 필요한 경우에만 전역 변수 예제와 유사한 around 블록을 사용할 수 있지만 가능한한 피하는 것이 좋습니다.

Elasticsearch 스펙

Elasticsearch를 필요로하는 스펙은 :elastic 트레이트로 표시해야 합니다.
이는 모든 예제 앞뒤에 인덱스를 생성하고 삭제합니다.

:elastic_delete_by_query 트레이트는 파이프라인 실행 시간을 줄이기 위해 맥락의 시작과 끝에서 인덱스를 생성하고 삭제합니다.
검색 결과, 깨끗한 인덱스를 보장하기 위해 예제 간에 모든 인덱스(마이그레이션 인덱스 제외)의 데이터를 삭제하는 Elasticsearch delete by query API가 사용됩니다.

:elastic_clean 트레이트는 깨끗한 인덱스를 보장하기 위해 예제 간에 인덱스를 생성하고 삭제합니다. 이렇게 하면 테스트가 비필요한 데이터로 오염되지 않습니다. 만약 :elastic 또는 :elastic_delete_by_query 트레이트를 사용하는 것이 문제를 일으키면, 대신 :elastic_clean을 사용하세요. :elastic_clean은 다른 트레이트에 비해 상당히 느리며, 절약해서 사용해야 합니다.

Elasticsearch 논리에 대한 대부분의 테스트는 다음과 관련이 있습니다:

  • PostgreSQL에 데이터를 생성하고 Elasticsearch에서 인덱싱 완료를 기다립니다.
  • 해당 데이터를 검색합니다.
  • 테스트가 예상한 결과를 얻는지 확인합니다.

또한, 인덱스 내 개별 레코드 대신 구조적인 변경 여부를 확인하는 등 일부 예외사항이 있습니다.

참고: Elasticsearch 인덱싱에는 Gitlab::Redis::SharedState가 사용됩니다.
따라서 Elasticsearch 트레이트는 동적으로 :clean_gitlab_redis_shared_state 트레이트를 사용합니다.
수동으로 :clean_gitlab_redis_shared_state를 추가할 필요가 없습니다.

Elasticsearch를 사용하는 스펙을 필요로 하기 때문에, 다음을 수행해야 합니다:

  • PostgreSQL에 데이터를 생성하고 Elasticsearch로 인덱싱합니다.
  • Elasticsearch 애플리케이션 설정을 활성화합니다 (기본적으로 비활성화됨).

이를 위해 다음을 사용하세요:

before do
  stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end

또한, Elasticsearch의 비동기적인 성격을 극복하기 위해 ensure_elasticsearch_index! 메서드를 사용할 수 있습니다.
이는 Elasticsearch Refresh API를 사용하여 마지막 새로고침 이후에 인덱스에 수행된 모든 작업을 검색 가능하게 합니다.
이 메서드는 일반적으로 PostgreSQL에 데이터를 로드한 후 데이터가 인덱싱되고 검색 가능하도록 하기 위해 호출됩니다.

테스트 설정 단계를 벤치마킹하려면 SEARCH_SPEC_BENCHMARK 환경 변수를 사용할 수 있습니다:

SEARCH_SPEC_BENCHMARK=1 bundle exec rspec ee/spec/lib/elastic/latest/merge_request_class_proxy_spec.rb

Snowplow 이벤트 테스트

경고: Snowplow는 contracts gem을 사용하여 런타임 타입 확인을 수행합니다.
Snowplow가 기본적으로 테스트 및 개발에서 비활성화되기 때문에 Gitlab::Tracking을 mocking할 때 예외를 catch하는 것이 어려울 수 있습니다.

타입 확인으로 인한 런타임 에러를 catch하기 위해 expect_snowplow_event를 사용할 수 있습니다.
이는 Gitlab::Tracking#event 호출을 확인합니다.

describe '#show' do
  it 'tracks snowplow events' do
    get :show

    expect_snowplow_event(
      category: 'Experiment',
      action: 'start',
      namespace: group,
      project: project
    )
    expect_snowplow_event(
      category: 'Experiment',
      action: 'sent',
      property: 'property',
      label: 'label',
      namespace: group,
      project: project
    )
  end
end

이벤트가 호출되지 않았음을 보장하려면 expect_no_snowplow_event를 사용할 수 있습니다.

  describe '#show' do
    it 'does not track any snowplow events' do
      get :show

      expect_no_snowplow_event(category: described_class.name, action: 'some_action')
    end
  end

categoryaction은 생략될 수 있지만, category를 적어도 지정해야만 합니다.
예를 들어, Users::ActivityService는 API 요청 이후 Snowplow 이벤트를 추적할 수 있으며,
expect_no_snowplow_event는 인수가 지정되지 않았을 때 실행될 경우 실패합니다.

테이블 기반 / 매개변수화된 테스트

이 스타일의 테스트는 다양한 입력 범위로 코드 조각을 실행하는 데 사용됩니다. 한 번의 테스트 케이스를 지정하여 각각의 기대 출력과 함께 테이블의 입력을 사용하면 테스트가 읽기 쉽고 더 간결해집니다.

우리는 RSpec::Parameterized 젬을 사용합니다. 테이블 구문을 사용하여 입력 범위를 확인하고 루비 동등성을 확인하는 간단한 예는 다음과 같습니다.

describe "#==" do
  using RSpec::Parameterized::TableSyntax

  let(:one) { 1 }
  let(:two) { 2 }

  where(:a, :b, :result) do
    1         | 1         | true
    1         | 2         | false
    true      | true      | true
    true      | false     | false
    ref(:one) | ref(:one) | true  # let 변수는 `ref`를 사용하여 참조되어야 합니다
    ref(:one) | ref(:two) | false
  end

  with_them do
    it { expect(a == b).to eq(result) }

    it 'is isomorphic' do
      expect(b == a).to eq(result)
    end
  end
end

테이블 기반의 테스트를 작성한 후 다음과 같은 오류가 발생하는 경우:

NoMethodError:
  undefined method `to_params'

  param_sets = extracted.is_a?(Array) ? extracted : extracted.to_params
                                                                       ^^^^^^^^^^
  Did you mean?  to_param

이는 테스트 파일에 using RSpec::Parameterized::TableSyntax 라인을 추가해야 함을 나타냅니다.

경고: where 블록에 단순한 값만 입력으로 사용하세요. 프록, 상태를 가진 객체, FactoryBot로 생성된 객체 및 유사한 항목을 사용하는 것은 예상치 못한 결과를 유발할 수 있습니다.

프로메테우스 테스트

프로메테우스 메트릭은 한 번의 테스트 실행에서 다음으로 전달될 수 있습니다. 메트릭이 각 예제 전에 재설정되도록 하려면 RSpec 테스트에 :prometheus 태그를 추가하세요.

Matchers

RSPec 기대와 복잡성을 명확하게 하기 위해 사용자 정의 Matchers를 생성해야 합니다. 이들은 spec/support/matchers/ 아래에 배치되어야 합니다. Matchers는 특정 유형의 스펙에만 적용되는 경우에는 하위 폴더에 배치할 수 있지만(예: 기능 또는 요청), 여러 유형의 스펙에 적용되는 경우에는 그렇게 하지 말아야 합니다.

be_like_time

데이터베이스로부터 반환된 시간은 루비의 시간 객체와 정밀도가 다를 수 있으므로 스펙에서 비교할 때 유연한 허용 범위가 필요합니다.

PostgreSQL의 시간 및 타임스탬프 유형은 마이크로초의 정밀도를 가집니다. 그러나 루비 Time의 정밀도는 운영 체제에 따라 다를 수 있습니다.

다음 스니펫을 살펴보세요.

project = create(:project)

expect(project.created_at).to eq(Project.find(project.id).created_at)

리눅스의 경우 Time은 최대 9의 정밀도를 가질 수 있으며 project.created_at에는 해당 정밀도를 가진 값(예: 2023-04-28 05:53:30.808033064)이 있습니다. 그러나 실제 값 created_at(예: 2023-04-28 05:53:30.808033)는 데이터베이스에 저장되고 로드될 때 해당 정밀도를 가지지 않으며 일치하지 않을 수 있습니다. macOS X에서는 Time의 정밀도가 PostgreSQL의 타임스탬프 유형과 일치하며 일치할 수 있습니다.

이 문제를 피하려면 시간이 서로 1초 이내인지 비교하기 위해 be_like_time이나 be_within을 사용할 수 있습니다.

예시:

expect(metrics.merged_at).to be_like_time(time)

be_within의 예시:

expect(violation.reload.merged_at).to be_within(0.00001.seconds).of(merge_request.merged_at)

have_gitlab_http_status

have_gitlab_http_status를 사용하여 have_http_statusexpect(response.status).to 대신 사용하세요. 후자는 상태 불일치 시 응답 본문을 표시할 수 있으며, 일부 테스트가 실패한 이유를 알고 싶을 때 매우 유용합니다.

특히 500 내부 서버 오류가 표시될 때 매우 유용합니다.

숫자 표현 대신에 :no_content와 같은 명명된 HTTP 상태를 선호하세요. 지원되는 상태 코드 목록을 참조하세요.

예시:

expect(response).to have_gitlab_http_status(:ok)

match_schemamatch_response_schema

match_schema 매처를 사용하면 주제가 JSON 스키마와 일치하는지 확인할 수 있습니다. expect 내부의 항목은 JSON 문자열 또는 JSON 호환 데이터 구조일 수 있습니다.

match_response_schema는 응답 객체와 함께 사용하기 위한 편리한 매처입니다. 요청 사양에서 사용됩니다.

예시:

# spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json에 일치합니다.
expect(data).to match_schema('prometheus/additional_metrics_query_result')

# ee/spec/fixtures/api/schemas/board.json에 일치합니다.
expect(data).to match_schema('board', dir: 'ee')

# Ruby 데이터 구조로 이루어진 스키마에 일치합니다.
expect(data).to match_schema(Atlassian::Schemata.build_info)

be_valid_json

be_valid_json을 사용하면 문자열이 JSON으로 구문 분석되고 비어 있지 않은 결과가 나온다는 것을 확인할 수 있습니다. 위의 스키마 매칭과 결합하기 위해 and를 사용하세요:

expect(json_string).to be_valid_json

expect(json_string).to be_valid_json.and match_schema(schema)

be_one_of(collection)

include의 반대로, collection이 예상 값에 포함되어 있는지를 테스트합니다.

expect(:a).to be_one_of(%i[a b c])
expect(:z).not_to be_one_of(%i[a b c])

쿼리 성능 테스트

쿼리 성능 테스트를 통해 다음을 할 수 있습니다:

  • 코드 블록에서 N+1 문제가 존재하지 않음을 확인합니다.
  • 코드 블록에서의 쿼리 수가 알림 없이 증가하지 않도록 보장합니다.

QueryRecorder

QueryRecorder를 사용하면 주어진 코드 블록에서 수행된 데이터베이스 쿼리 수를 프로파일링하고 테스트할 수 있습니다.

자세한 내용은 QueryRecorder 섹션을 참조하세요.

GitalyClient

Gitlab::GitalyClient.get_request_count를 사용하여 주어진 코드 블록에서 수행된 Gitaly 쿼리 수를 테스트할 수 있습니다:

자세한 내용은 Gitaly Request Counts 섹션을 참조하세요.

공유된 컨텍스트

한 개의 spec 파일에서만 사용되는 공유된 컨텍스트는 인라인으로 선언할 수 있습니다. 여러 개의 spec 파일에서 사용되는 공유된 컨텍스트:

  • spec/support/shared_contexts/ 아래에 위치해아 합니다.
  • 특정 유형의 spec에만 해당되는 경우 하위 폴더에 배치할 수 있지만 여러 유형의 spec에 적용되는 경우에는 배치해서는 안 됩니다.

각 파일은 하나의 컨텍스트만 포함하고 spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb와 같이 설명적인 이름을 가져야 합니다.

공유된 예시

한 개의 spec 파일에서만 사용되는 공유된 예시는 인라인으로 선언할 수 있습니다. 여러 개의 spec 파일에서 사용되는 공유된 예시:

  • spec/support/shared_examples/ 아래에 위치해야 합니다.
  • 특정 유형의 spec에만 해당되는 경우 하위 폴더에 배치할 수 있지만 여러 유형의 spec에 적용되는 경우에는 배치해서는 안 됩니다.

각 파일은 하나의 컨텍스트만 포함하고 spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb와 같이 설명적인 이름을 가져야 합니다.

헬퍼

헬퍼는 주로 특정 RSpec 예시의 복잡성을 숨기기 위해 일부 메서드를 제공하는 모듈입니다. 다른 spec와 공유되지 않는 경우 RSpec 파일에 헬퍼를 정의할 수 있습니다. 그렇지 않으면 spec/support/helpers/ 아래에 배치해야 합니다. 헬퍼는 특정 유형의 spec에만 해당되는 경우 하위 폴더에 배치할 수 있지만, 여러 유형의 spec에 적용되는 경우에는 배치해서는 안 됩니다.

헬퍼는 Rails 네이밍 / 네임스페이싱 규칙을 따라야 합니다. 예를 들어 spec/support/helpers/features/iteration_helpers.rb는 다음과 같이 정의해야 합니다:

# frozen_string_literal: true

module Features
  module IterationHelpers
    def iteration_period(iteration)
      "#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
    end
  end
end

헬퍼는 RSpec 구성을 변경해서는 안 됩니다. 예를 들어, 위의 헬퍼 모듈은 다음과 같이 포함해서는 안 됩니다:

# 나쁨
RSpec.configure do |config|
  config.include Features::IterationHelpers
end

# 좋음, 특정 spec에 포함
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
  include Features::IterationHelpers
end

Ruby 상수 테스트

Ruby 상수를 사용하는 코드를 테스트할 때, 상수 값을 테스트하는 것보다 상수에 따라 달라지는 동작을 테스트하는 것에 초점을 맞추세요.

예를 들어, 다음과 같이 하는 것이 권장됩니다. 이는 .categories 클래스 메서드의 동작을 테스트하기 때문입니다.

describe '.categories' do
  it 'gets CE unique category names' do
    expect(described_class.categories).to include(
      'deploy_token_packages',
      'user_packages',
      # ...
      'kubernetes_agent'
    )
  end
end

반면에, 상수 값 자체를 테스트하는 것은 대부분 코드와 테스트에서 값들을 반복하기 때문에 그다지 유용하지 않습니다.

예를 들어, 다음과 같이 상수 값 자체를 테스트하는 것은 잘못된 접근입니다.

describe CATEGORIES do
  it 'has values' do
    expect(CATEGORIES).to eq([
                            'deploy_token_packages',
                            'user_packages',
                            # ...
                            'kubernetes_agent'
                             ])
  end
end

상수 값에 따라 치명적인 영향을 미칠 수 있는 경우, 상수의 오류가 치명한 영향을 미칠 수 있는 경우에는 상수 값 테스트가 추가적인 안전장치로써 유용할 수 있습니다. 예를 들어, 전체 GitLab 서비스를 멈추거나 고객에게 과다 청구를 할 수 있는 경우, 또는 우주를 함락시킬 수 있는 경우와 같은 상황입니다.

팩토리

GitLab은 테스트 픽스처를 대체하기 위해 factory_bot을 사용합니다.

  • 팩토리 정의는 spec/factories/에 있으며 해당 모델의 복수형을 사용하여 명명됩니다 (User 팩토리는 users.rb에 정의됨).
  • 파일당 최상위 팩토리 정의는 하나여아 합니다.
  • FactoryBot 메서드는 모든 RSpec 그룹에 혼합됩니다. 즉, FactoryBot.create(...) 대신에 create(...)을 호출해야 합니다.
  • traits를 사용하여 정의와 사용을 정리하세요.
  • 팩토리를 정의할 때, 결과 레코드가 유효성 검사를 통과하는 데 필요하지 않은 속성은 정의하지 마십시오.
  • 팩토리에서 생성할 때, 테스트에 필요하지 않은 속성을 제공하지 마십시오.
  • 콜백에서 연결 설정에 암시적, 명시적, 또는 인라인 연결을 위해 create / build 대신 사용하십시오. 추가 맥락은 이슈 #262624를 참조하세요.

    has_manybelongs_to 연결을 사용하여 팩토리를 생성할 때, instance 메서드를 사용하여 빌드 중인 객체를 참조하십시오. 이렇게 하면 중요하지 않은 레코드의 생성상호 연결된 연결을 사용함으로써 방지됩니다.

    예를 들어, 다음과 같은 클래스가 있다면:

    class Car < ApplicationRecord
      has_many :wheels, inverse_of: :car, foreign_key: :car_id
    end
    
    class Wheel < ApplicationRecord
      belongs_to :car, foreign_key: :car_id, inverse_of: :wheel, optional: false
    end
    

    다음과 같은 팩토리를 생성할 수 있습니다:

    FactoryBot.define do
      factory :car do
        transient do
          wheels_count { 2 }
        end
    
        wheels do
          Array.new(wheels_count) do
            association(:wheel, car: instance)
          end
        end
      end
    end
    
    FactoryBot.define do
      factory :wheel do
        car { association :car }
      end
    end
    
  • 팩토리는 ActiveRecord 객체에만 한정되어 있을 필요가 없습니다. 예시를 참조하세요.
  • 팩토리 및 해당 traits는 검증된 스펙에 의해 확인된 유효한 객체를 만들어야 합니다.
  • 팩토리에서 skip_callback 사용을 피하십시오. 자세한 내용은 이슈 #247865를 참조하세요.

픽스처

모든 픽스처는 spec/fixtures/ 아래에 배치되어야 합니다.

저장소

병합 요청을 병합하는 등의 기능을 테스트하려면 특정 상태의 Git 리포지토리가 테스트 환경에 있어야 합니다. GitLab은 일부 일반적인 경우를 위해 gitlab-test 리포지토리를 유지합니다. 프로젝트 팩토리에 :repository trait을 사용하여 리포지토리의 사본이 사용되도록 할 수 있습니다:

let(:project) { create(:project, :repository) }

가능하다면 :repository 대신에 :custom_repo trait을 사용하는 것을 고려하세요. 이를 통해 프로젝트 리포지토리의 main 브랜치에 정확히 어떤 파일이 나타나는지를 지정할 수 있습니다. 예시:

let(:project) do
  create(
    :project, :custom_repo,
    files: {
      'README.md'       => 'Content here',
      'foo/bar/baz.txt' => 'More content here'
    }
  )
end

이렇게 하면 기본 권한과 지정된 내용을 포함하는 파일 두 개가 있는 리포지토리가 생성됩니다.

구성

RSpec 구성 파일은 RSpec 구성을 변경하는 파일 (RSpec.configure do |config| 블록과 같은)입니다. 이러한 파일은 spec/support/에 배치되어야 합니다.

각 파일은 spec/support/capybara.rb 또는 spec/support/carrierwave.rb와 같이 특정 도메인과 관련이 있어야 합니다.

만약 헬퍼 모듈이 특정 종류의 스펙에만 적용된다면, config.include 호출에 수정자를 추가해야 합니다. 예를 들어 spec/support/helpers/cycle_analytics_helpers.rb:libtype: :model 스펙에만 적용된다면 다음과 같이 작성해야 합니다:

RSpec.configure do |config|
  config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
  config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
end

만약 구성 파일이 config.include로만 구성된 경우 config.includespec/spec_helper.rb에 직접 추가할 수 있습니다.

매우 일반적인 헬퍼의 경우 spec/support/rspec.rb 파일에 포함하는 것을 고려하세요. 이 파일은 spec/fast_spec_helper.rb 파일에서 사용됩니다. spec/fast_spec_helper.rb 파일에 대한 자세한 내용은 빠른 유닛 테스트를 참조하세요.

테스트 환경 로깅

테스트를 실행할 때 Gitaly, Workhorse, Elasticsearch, 및 Capybara를 포함한 테스트 환경의 서비스는 자동으로 구성되고 시작됩니다. CI에서 실행하거나 서비스를 설치해야 할 경우, 테스트 환경은 설정 시간에 대한 정보와 함께 다음과 같은 로그 메시지를 생성합니다.

==> Gitaly 설정 중...
    Gitaly가 31.459649초에서 설정됨...

==> GitLab Workhorse 설정 중...
    GitLab Workhorse가 29.695619초에서 설정됨...
fatal: update refs/heads/diff-files-symlink-to-image: invalid <newvalue>: 8cfca84
From https://gitlab.com/gitlab-org/gitlab-test
 * [new branch]      diff-files-image-to-symlink -> origin/diff-files-image-to-symlink
 * [new branch]      diff-files-symlink-to-image -> origin/diff-files-symlink-to-image
 * [new branch]      diff-files-symlink-to-text -> origin/diff-files-symlink-to-text
 * [new branch]      diff-files-text-to-symlink -> origin/diff-files-text-to-symlink
   b80faa8..40232f7  snippet/multiple-files -> origin/snippet/multiple-files
 * [new branch]      testing/branch-with-#-hash -> origin/testing/branch-with-#-hash

==> GitLab Elasticsearch Indexer 설정 중...
    GitLab Elasticsearch Indexer가 26.514623초에서 설정됨...

로컬에서 실행하거나 작업을 수행할 필요가 없을 때는 이 정보가 생략됩니다. 이러한 메시지를 항상 표시하고 싶은 경우 다음 환경 변수를 설정하세요.

GITLAB_TESTING_LOG_LEVEL=debug

테스트 문서로 돌아가기