테스트의 최상의 사례

테스트 디자인

GitLab에서의 테스트는 후속 조치가 아닌 최우선 사안으로 간주됩니다. 기능 설계와 마찬가지로 테스트의 디자인을 고려하는 것이 중요합니다.

기능을 구현할 때 우리는 올바른 능력을 올바른 방식으로 개발하는 데에 대해 고민합니다. 이는 우리의 범위를 관리 가능한 수준으로 좁히는 데 도움이 됩니다. 기능에 대한 테스트를 구현할 때 우리는 올바른 테스트를 개발해야 하지만, 그러한 테스트가 실패할 수 있는 중요한 방법을 모두 고려해야 합니다. 이러한 고려는 단기간에 우리의 범위를 관리하기 어려운 수준으로 확대시킬 수 있습니다.

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

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를 사용하세요.
  • 시퀀스로 생성된 속성의 절대 값에 대해 단언하지 마세요 (주의점 참조).
  • expect_any_instance_of 또는 allow_any_instance_of을 사용하지 마세요 (주의점 참조).
  • ‘each’ 인수를 훅에 제공하지 마세요. 왜냐하면 그것이 기본값이기 때문입니다.
  • ‘before’와 ‘after’ 훅에서는 :all 대신 :context에 대해 범위를 좁게 하는 것을 선호하세요.
  • 주어진 요소에 작용하는 evaluate_script("$('.js-foo').testSomething()") (또는 execute_script)을 사용할 때, 실제로 요소가 존재하는지 확인하기 위해 먼저 Capybara 매처(예: find('.js-foo'))를 사용하세요.
  • 실행하려는 스펙의 일부를 격리하기 위해 focus: true를 사용하세요.
  • 테스트에 여러 기대값이 있는 경우 :aggregate_failures를 사용하세요.
  • 빈 테스트 설명 블록의 경우 테스트가 자명한 경우 specify 대신 it do을 사용하세요.
  • 실제로 존재하지 않는 ID/IID/접근 수준이 필요한 경우 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를 사용하여 테스트가 소비하는 시간을 보여주는 화염그래프를 생성할 수 있습니다.

이 젬은 우리가 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
화염그래프 해석 방법

다음은 화염그래프를 해석하고 탐색하는 유용한 팁입니다:

  • 화염그래프에는 여러 가지 보기가 있습니다. 많은 함수 호출(예: feature 스펙)이 있는 경우 Left Heavy가 특히 유용합니다.
  • 확대/축소할 수 있습니다! 탐색 설명서를 참조하세요.
  • 느린 feature 테스트를 작업 중이라면 검색하여 Capybara::DSL#에서 capybara 동작과 소요 시간을 확인하세요!

분석 예제는 #414929 또는 #375004을 참조하세요.

Factory 사용 최적화

느린 테스트의 일반적인 원인은 객체들을 과도하게 생성하고, 이로 인해 계산 및 DB 시간이 소요됩니다. Factory는 개발에 필수적이지만, 데이터를 쉽게 DB에 삽입할 수 있어 최적화할 수 있는 경우가 있습니다.

여기서 주의해야 할 두 가지 기본 기술은 다음과 같습니다.

  • 감소: 객체 생성을 피하고, 지속되지 않게 합니다.
  • 재사용: 특히 우리가 검토하지 않는 중첩된 공유 객체는 일반적으로 공유할 수 있습니다.

생성을 피하기 위해서 다음 사항을 염두에 둘 가치가 있습니다.

  • instance_doublespyFactoryBot.build(...)보다 빠릅니다.
  • FactoryBot.build(...).build_stubbed.create보다 빠릅니다.
  • 데이터베이스 지속성이 느립니다!

데이터베이스 지속성이 필요하지 않은 경우 Factory Doctor를 사용하여 테스트에 필요하지 않은 데이터베이스 지속성을 찾을 수 있습니다.

Factory 최적화의 예시 1, 2.

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

create 대신 build 또는 build_stubbed를 사용하는 일반적인 변경 사항은 다음과 같습니다.

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

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

Factory Profiler를 사용하여 Factory를 통해 반복되는 데이터베이스 지속성을 식별할 수 있습니다.

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

# 화염그래프와 함께 시각화
FPROF=flamegraph bin/rspec spec/[path]/[to]/[spec].rb

많은 수의 생성된 팩토리는 팩토리 카스케이드의 일반적인 원인이며, 팩토리가 형성되고 연관을 다시 형성할 때 발생합니다. 명백한 차이점으로 확인할 수 있습니다.

   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을 사용할 수 있습니다.

let에 대해 이야기해 봅시다

테스트에서 객체를 생성하고 변수에 저장하는 여러 가지 방법이 있습니다. 가장 비효율적인 방법부터 가장 효율적인 방법까지 다음과 같습니다:

  • let!은 각 예제가 실행되기 전에 객체를 생성합니다. 또한 각 예제마다 새 객체를 생성합니다. 명시적으로 참조하지 않고 각 예제마다 깨끗한 객체를 생성해야 하는 경우에만 이 옵션을 사용해야 합니다.
  • let은 객체를 지연해서 생성합니다. 객체가 호출될 때까지 만들어지지 않습니다. let은 일반적으로 각 예제마다 새 객체를 만들기 때문에 비효율적입니다. 그러나 let은 간단한 값에 대해서는 괜찮습니다. 그러나 데이터베이스 모델(팩토리 등)과 같은 보다 효율적인 let의 변형이 있는 것이 좋습니다.
  • let_it_be_with_refindlet_it_be_with_reload와 유사하게 작동하지만 전자는 ActiveRecord::Base#find를 호출하는 반면 ActiveRecord::Base#reload는 보통 refind보다 빠릅니다.
  • let_it_be_with_reload는 한 번 객체를 생성하여 동일한 문맥 내의 모든 예제에 대해 만듭니다. 그러나 각 예제 후에 데이터베이스 변경이 롤백되며 object.reload가 호출되어 객체를 원래 상태로 복원합니다. 이는 예제에서 객체를 변경할 수 있음을 의미합니다. 그러나 다른 모델들 사이에 상태 누출이 발생할 수 있는 경우도 있습니다. 이러한 경우에서는 특히 예제가 몇 개만 있는 경우에는 let이 더 간단한 옵션이 될 수 있습니다.
  • let_it_be는 같은 문맥 내의 모든 예제에 대해 한 번 객체를 생성합니다. 이는 letlet! 대신 사용하기 좋은 대안입니다. 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_it_be 대신에 let이 필요한 경우 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_reloadlet보다 더 효율적인 경우의 예시입니다:

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

context '개발자와 함께' do
  before do
    project.add_developer(user)
  end
  
  it '프로젝트에는 소유자와 개발자가 있습니다' do
    expect(project.members.map(&:access_level)).to match_array([Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER])
  end
end

context '관리자와 함께' do
  before do
    project.add_maintainer(user)
  end
  
  it '프로젝트에는 소유자와 관리자가 있습니다' do
    expect(project.members.map(&:access_level)).to match_array([Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER])
  end
end

팩토리 내에서 메서드 스텁 생성

팩토리에서 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
note
stub_methodlet_it_be_with_refind와 함께 사용될 때 작동하지 않습니다. 이는 stub_method가 인스턴스의 메서드를 스텁하고 let_it_be_with_refind가 각 실행마다 객체의 새 인스턴스를 만들기 때문입니다.

stub_method는 메서드 존재 및 메서드 아리티(arity) 확인을 지원하지 않습니다.

caution
stub_method는 팩토리에서만 사용해야 합니다. 다른 곳에서 사용하는 것은 권장되지 않습니다. 가능하다면 RSpec mocks를 사용해 보세요.

Member Access Level 스텁 만들기

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
note
테스트 코드가 project_authorizations 또는 Member 레코드를 지속하는 데 의존하는 경우에는 이 스텁 도우미를 사용하지 마세요. 대신 Project#add_member 또는 Group#add_member를 사용하세요.

추가 프로파일링 메트릭

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

이는 일부 응용 프로그램 측 SQL 쿼리들이 테스트에 의해 일으킨 것일 수 있습니다. 이것은 테스트의 대상이 아닌 부분을 모의화할 수 있는 경우입니다 (예: !123810).

성능 문서의 지침을 참조하세요.

느린 기능 테스트 문제 해결

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

UI에서 기능 테스트 동작 확인하기
# 이전
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

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

가장 느린 예제 상위 10개 (전체 시간의 7.7%, 10.69초):
  Issue behaves like an editable mentionable creates new cross-reference notes when the mentionable text is edited
    1.62초 ./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초 ./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초 ./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초 ./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초 ./spec/support/shared_examples/models/mentionable_shared_examples.rb:101
  ...

이 결과에서 우리는 가장 소비가 많은 예제를 볼 수 있으며, 여기서 개선을 시작할 수 있습니다. 여기서 가장 비싼 예제는 공유 예제에 있으며, 이러한 감소는 일반적으로 여러 곳에서 호출되기 때문에 더 큰 영향을 미칩니다.

가장 느린 테스트

우리는 rspec_profiling_stats 프로젝트에서 테스트 지속 시간에 대한 정보를 수집합니다. 이 정보는 GitLab Pages를 통해 UI에서 표시됩니다.

우리는 테스트가 지속 기간을 충족하지 못하는 경우, 이슈를 자동으로 생성하여 개선합니다.

합법적인 이유로 느린 테스트가 있는 경우 이슈 생성을 건너뛰고 건너뛸 수 있도록 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를 검색하거나 the www-gitlab-org project에서 바로 찾아보세요.

기능 카테고리 메타데이터

각 RSpec 예시에 대해 기능 카테고리 메타데이터를 설정해야 합니다.

EE 라이선스에 따라 테스트

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

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

SaaS에 따라 테스트

saas RSpec 메타데이터 태그 헬퍼를 사용하여 GitLab.com에서만 실행되는 코드를 테스트하는 context/spec 블록에서 사용할 수 있습니다. 이 헬퍼는 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%를 커버하는지 확인하기 위해 커버리지 보고서를 사용하세요.

시스템 / 기능 테스트

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

UI 테스트

UI를 테스트할 때는 사용자가 볼 수 있는 것과 상호작용하는 것을 모방하는 테스트를 작성하세요. 이는 Capybara의 세맨틱한 메서드를 선호하고 ID, 클래스 또는 속성으로 쿼리하는 것을 피하는 것을 의미합니다.

이러한 방식으로 테스트하는 것의 장점은 다음과 같습니다.

  • 모든 상호 작용 요소에 대해 스크린 리더에 대한 접근 가능한 이름을 보장합니다.
  • 자연어를 사용하여 더 읽기 쉽습니다.
  • 사용자에게 표시되지 않는 ID, 클래스 및 속성으로 쿼리하는 것을 피하기 때문에 취약하기에 덜합니다.

필요하다면, ID, 클래스 이름 또는 data-testid 대신 엘리먼트의 텍스트 레이블로 쿼리하세요.

within을 사용하여 페이지의 특정 영역 내의 상호작용을 제한할 수 있습니다. 보통 레이블이 없는 div와 같은 요소로 범위를 지정하기 때문에 이러한 경우 data-testid 선택기를 사용할 수 있습니다.

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

외부화된 콘텐츠

RSpec 테스트에서 외부화된 콘텐츠에 대한 기대치는 번역과 일치하도록 동일한 외부화 메서드를 호출해야 합니다. 예를 들어, Ruby에서 _ 메서드를 사용해야 합니다.

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

동작

가능한 경우, 아래의 동작과 같이 더 구체적인 동작을 사용하세요.

# good
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')

# bad - 상호작용 요소에는 접근 가능한 이름이 있어야 하므로
# 위의 특정 동작 중 하나를 사용할 수 있어야 합니다
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')
찾기기

가능한 경우 아래 항목과 같이 더 구체적인 finders를 사용하세요.

# 좋음
find_button _('제출 리뷰')
find_button _('제출 리뷰'), disabled: true

find_link _('UI 테스트 문서')
find_link _('UI 테스트 문서'), href: docs_url

find_field _('프로젝트 검색')
find_field _('프로젝트 검색'), with: 'gitlab' # 텍스트가 포함된 입력 필드 찾기
find_field _('프로젝트 검색'), disabled: true
find_field _('체크박스 라벨'), checked: true
find_field _('체크박스 라벨'), unchecked: true

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

가능한 경우 아래 항목과 같이 더 구체적인 matchers를 사용하세요.

# 좋음
expect(page).to have_button _('제출 리뷰')
expect(page).to have_button _('제출 리뷰'), disabled: true
expect(page).to have_button _('알림'), class: 'is-checked' # "알림" GlToggle이 선택되었음을 단언

expect(page).to have_link _('UI 테스트 문서')
expect(page).to have_link _('UI 테스트 문서'), href: docs_url # 링크에 href 속성이 있음을 단언

expect(page).to have_field _('프로젝트 검색')
expect(page).to have_field _('프로젝트 검색'), disabled: true
expect(page).to have_field _('프로젝트 검색'), with: 'gitlab' # 입력 필드에 텍스트가 있음을 단언

expect(page).to have_checked_field _('체크박스 라벨')
expect(page).to have_unchecked_field _('라디오 입력 라벨')

expect(page).to have_select _('정렬 기준')
expect(page).to have_select _('정렬 기준'), selected: '최근 업데이트일' # 옵션이 선택되었음을 단언
expect(page).to have_select _('정렬 기준'), options: ['최근 업데이트일', '생성일', '마감일'] # 정확한 옵션 디렉터리을 단언
expect(page).to have_select _('정렬 기준'), with_options: ['생성일', '마감일'] # 일부 옵션 디렉터리을 단언

expect(page).to have_text _('일부 문단 텍스트.')
expect(page).to have_text _('일부 문단 텍스트.'), exact: true # 정확한 일치를 단언

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

expect(page).to have_title _('찾을 수 없음')

# 더 구체적인 매처가 불가능한 경우 허용됨
expect(page).to have_css 'h2', text: '이슈 제목'
expect(page).to have_css 'p', text: '이슈 설명', exact: true
expect(page).to have_css '[data-testid="weight"]', text: 2
expect(page).to have_css '.atwho-view ul', visible: true
모달과 상호 작용하기

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

include Spec::Support::Helpers::ModalHelpers

within_modal do
  expect(page).to have_link _('UI 테스트 문서')
  
  fill_in _('프로젝트 검색'), with: 'gitlab'
  
  click_button '계속'
end

또한, accept_gl_confirm를 사용하여 수락만 필요한 확인 모달에 사용할 수 있습니다. 이 기능은 window.confirm()confirmAction로 마이그레이션할 때 유용합니다.

include Spec::Support::Helpers::ModalHelpers

accept_gl_confirm do
  click_button '사용자 삭제'
end

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

include Spec::Support::Helpers::ModalHelpers

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

finder method를 사용하여 요소를 검색한 후 hover와 같은 여러 element methods를 호출할 수 있습니다.

Capybara 테스트에는 accept_confirm과 같은 다양한 session methods도 있습니다.

일부 다른 유용한 메서드는 아래와 같습니다.

refresh # 페이지 새로고침

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

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

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

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

#### 실시간 디버그

가끔은 브라우저 동작을 관찰하여 Capybara 테스트를 디버그해야  때가 있습니다.

스펙에서 `live_debug` 메서드를 사용하여 Capybara 일시 중지하고 현재 페이지를 기본 브라우저에서 확인할  있습니다.
처음으로 로그인해야   있습니다(현재 사용자의 자격 증명이 터미널에 표시됩니다).

테스트를 계속하려면 아무 키나 누르세요.

:

```shell
$ bin/rspec spec/features/auto_deploy_spec.rb:34
Spring 사전로더를 통해 프로세스 8999에서 실행중
실행 옵션: {: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로부터 잘 분리되어 있습니다. Rails 환경과 Bundler의 :default 그룹의 gem 로딩으로 인한 오버헤드 없이 이를 테스트할 수 있어야 합니다. 이러한 경우에는 테스트 파일에서 require 'spec_helper' 대신에 require 'fast_spec_helper'를 사용하여 테스트를 실행하면 gem 로딩, Rails 앱 부팅, GitLab Shell 및 Gitaly 설정, 테스트 리포지터리 설정이 모두 생략되어 테스트가 매우 빨리 실행됩니다.

fast_spec_helper는 또한 lib/ 디렉터리에 있는 코드만 사용하는 클래스를 자동으로 로드할 수 있습니다. 따라서 클래스나 모듈이 lib/ 디렉터리의 코드만 사용한다면 명시적으로 어떠한 의존성도 로드할 필요가 없습니다. fast_spec_helper는 Rails 환경에서 일반적으로 사용되는 핵심 확장 프로그램을 모두 로드합니다.

일부 경우에는 여전히 lib/에 없는 의존성을 몇 개 로드해야 할 수 있습니다. 이 경우도 require_dependency를 사용해야 합니다.

예를 들어, Gitlab::UntrustedRegexp 클래스를 사용하는 코드를 테스트하려면 re2 라이브러리를 사용합니다. 이를 위해서는:

  • re2 gem을 필요로 하는 라이브러리 파일에 require_dependency 're2'를 추가하여 이 요구사항을 명확하게 해야 합니다.
  • 스펙 자체에 추가합니다.
  • 루비콥 관련 스펙에 대해 rubocop_spec_helper를 사용합니다.

fast_spec_helper를 사용하는 테스트를 로드하려면 일반 spec_helper를 사용하는 경우보다 30초 이상이 아닌 1초 이내에 로드됩니다.

caution
코드와 해당 스펙이 Rails로부터 잘 분리되어 있는지 확인하려면 bin/rspec을 통해 스펙을 개별적으로 실행해야 합니다. bin/spring rspecspec_helper를 자동으로 로드하므로 사용하지 마세요.

subjectlet 변수

GitLab RSpec 스위트는 복제를 줄이기 위해 let (엄격 버전의 let!와 함께) 변수를 널리 사용했습니다. 하지만 때로는 명확성에 손해가 갈 수 있으므로 앞으로 그들의 사용에 대한 일부 지침을 설정해야 합니다.

  • let! 변수는 인스턴스 변수보다 선호됩니다. let 변수는 let! 변수보다 선호됩니다. 지역 변수는 let 변수보다 선호됩니다.
  • 전체 스펙 파일 내에서 중복을 줄이기 위해 let을 사용하세요.
  • 단일 테스트에서 사용되는 변수를 정의하기 위해 let을 사용하지 마세요. 해당 변수들은 테스트의 it 블록 내부의 지역 변수로 정의하세요.
  • describe 블록의 최상위 레벨에서 더 깊이 중첩된 contextdescribe 블록에서만 사용되는 let 변수를 정의하지 마세요. 사용되는 곳에 가까운 곳에 정의하세요.
  • 하나의 let 변수의 정의를 다른 변수로 덮어쓰지 않도록 하세요.
  • 다른 변수의 정의에만 사용되는 let 변수를 정의하지 마세요. 대신 도우미 메서드를 사용하세요.
  • 엄격한 평가 및 정의된 순서가 필요한 경우에만 let! 변수를 사용하세요. 그 외에는 let이 충분합니다. ‘let’은 게으르기 때문에 참조할 때까지 평가되지 않습니다.
  • 예제에서 subject를 참조하는 것을 피하세요. 대신 명명된 서브젝트 subject(:name)이나 let 변수를 사용하여 변수에 맥락을 부여하세요.
  • subject가 예제에서 사용되지 않는 경우에는 이름 없이 subject를 정의하는 것이 허용됩니다.

공통 테스트 설정

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

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

가능한 한 before(:all)이나 before(:context)을 사용하지 말아 주세요. 사용해야 하는 경우에는 데이터베이스 트랜잭션 외부에서 실행되므로 데이터를 매뉴얼으로 정리해야 합니다.

대신에 test-prof 젬에서 제공하는 let_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_be 블록 내에서 정의된 객체를 수정하는 경우, 다음 중 하나를 해야 합니다:

  • 필요한 대로 객체를 다시로드합니다.
  • let_it_be_with_reload에이스 별칭을 사용합니다.
  • 예제마다 다시로드할 수 있도록 reload 옵션을 지정합니다.
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:project, reload: true) { create(:project) }

또한 새 객체를 완전히 불러오기 위해 let_it_be_with_refind 별칭을 사용하거나 refind 옵션을 지정할 수 있습니다.

let_it_be_with_refind(:project) { create(:project) }
let_it_be(:project, refind: true) { create(:project) }

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의 블로그 글에서 가져왔습니다.

이 문제가 발생한 Merge Request백엔드 페어링 세션을 확인할 수 있습니다.

테스트 내에서의 피처 플래그

이 섹션은 피처 플래그로 개발하기로 이동되었습니다.

원시적인 테스트 환경

단일 GitLab 테스트에서 실행되는 코드는 많은 데이터 항목에 액세스하고 수정할 수 있습니다. 주의 깊게 준비되지 않은 테스트가 실행되면 테스트 이후에 정리되지 않은 데이터가 테스트의 동작에 영향을 미칠 수 있습니다. 이는 반드시 피해야 합니다! 다행히도 기존의 테스트 프레임워크는 이미 대부분의 경우를 처리합니다.

테스트 환경이 오염될 때 일반적인 결과는 불안정한 테스트입니다. 오염은 종종 순서 의존성으로 나타납니다: spec A를 먼저 실행한 다음 spec B를 실행하면 신뢰성 있게 실패하나, spec B를 먼저 실행한 다음 spec A를 실행하면 신뢰성 있게 성공합니다. 이러한 경우 rspec --bisect(또는 spec 파일의 매뉴얼 이진 검색)을 사용하여 무엇이 잘못 되었는지 확인할 수 있습니다. 문제를 해결하려면 테스트 스위트가 환경이 원시적임을 확인하는 방법을 이해해야 합니다. 각 데이터 스토어에 대해 자세히 알아보려면 계속 읽어보십시오!

SQL 데이터베이스

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

이러한 전략을 사용함으로써 잘 알려진 TRUNCATE TABLES 방식 대신에 사용하는 결과로 primary key 및 다른 시퀀스가 특정 spec 사이에서 재설정되지 않는다는 것입니다. 따라서 spec A에서 프로젝트를 생성하고, 다음으로 spec B에서 프로젝트를 생성한다면, 첫 번째는 id=1을 가지고 있고, 두 번째는 id=2를 가지게 됩니다.

이는 specs이 절대로 ID나 다른 시퀀스로 생성된 열의 값을 의존해서는 절대로 안 되며, 우연한 충돌을 피하기 위해 이러한 종류의 열에 대해 매뉴얼으로 값을 지정하는 것을 피해야 합니다. 대신에 이러한 열의 값을 지정하지 말고, 행이 생성된 후에 그 값을 조회해야 합니다.

마이그레이션 specs에서의 TestProf

위에서 설명한 바와 같이, 마이그레이션 specs는 데이터베이스 트랜잭션 내에서 실행할 수 없습니다. 테스트 스위트에서는 TestProf를 사용하여 런타임을 향상시키지만, TestProf는 이러한 최적화를 수행하기 위해 데이터베이스 트랜잭션을 사용합니다. 그러므로 이러한 이유로 우리는 마이그레이션 specs에서 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 인스턴스에서 지원되는 full list of Gitlab::Redis::Wrapper descendants를 확인할 수 있습니다.

대부분의 specs에서 Rails 캐시는 실제로 메모리에 저장됩니다. 이는 specs 간에 교체되므로 Rails.cache.readRails.cache.write 호출은 안전합니다. 그러나 spec이 직접적으로 Redis 호출을 수행하는 경우, 해당 spec은 적절한 :clean_gitlab_redis_cache, :clean_gitlab_redis_shared_state 또는 :clean_gitlab_redis_queues 특성을 표시해야 합니다.

백그라운드 작업 / Sidekiq

기본적으로, Sidekiq 작업은 jobs 배열에 인큐되고 처리되지 않습니다. 테스트가 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 특성을 사용하는 피처 specs에서 요율 제한이 트리거될 수 있습니다. 대부분의 경우, :clean_gitlab_redis_rate_limiting 특성을 테스트에 표시하여 요율 제한 데이터를 Redis 캐시에서 제거할 수 있습니다. 단일 테스트가 요율 제한을 트리거하는 경우, 대신 :disable_rate_limit를 사용할 수 있습니다.

파일 메서드 스터빙

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

어떠한 이유로든 File.read를 매뉴얼으로 스터빙해야 하는 경우에는 다음을 확인하세요:

  1. 다른 파일 경로에 대해 스터빙하고 원래 구현을 호출합니다.
  2. 그런 다음에 오직 원하는 파일 경로에 대해 File.read를 스터빙합니다.

그렇지 않으면 코드베이스의 다른 부분에서 올바르게 스터빙된 File.read 호출이 이루어지지 않습니다.

# bad, 모든 파일이 읽히고 아무것도 반환하지 않음
allow(File).to receive(:read)

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

# also OK
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(my_filepath).and_return("가짜 파일 내용")

파일 시스템

파일 시스템 데이터는 “리포지터리” 및 “기타 모든 것”으로 대략 분류될 수 있습니다. 리포지터리는 tmp/tests/repositories에 저장됩니다. 이 디렉터리는 테스트 실행 전에 비우고, 테스트 실행이 끝난 후에 비워집니다. 그러나 specs 간에 비워지지 않으므로 생성된 리포지터리는 프로세스의 수명 동안 이 디렉터리에 축적됩니다. 이를 삭제하는 것은 비용이 많이 들 수 있지만, 신중하게 관리하지 않으면 오염이 발생할 수 있습니다.

이를 피하기 위해 해시 저장가 테스트 스위트에서 활성화되어 있습니다. 이는 리포지터리가 그들의 프로젝트 ID에 따라 고유한 경로를 갖게 하므로, 프로젝트 IDs가 specs 사이에서 재설정되지 않기 때문에, 각 spec이 자체 디스크 상의 리포지터리를 얻게 되며, specs 사이에서 변경이 보이지 않도록 합니다.

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

ID에 의해 결정된 디스크에 쓰여지는 업로드와 같은 데이터베이스 객체에 연결된 파일들은 일반적으로 동일한 방식으로 관리됩니다. 테스트 스위트에서 hash 저장이 활성화되어 있으므로, 충돌이 발생하지 않아야 합니다. 일부 specs은 projects 팩토리에 :legacy_storage 특성을 전달함으로써 해시 저장을 비활성화합니다. 이렇게 하는 specs는 절대로 프로젝트의 path 또는 어떠한 그룹들을 오버라이드해서는 안 됩니다. 기본 경로에는 프로젝트 ID가 포함되어 있으므로, 충돌이 발생하지 않습니다. 같은 경로를 가진 두 개의 specs가 :legacy_storage 프로젝트를 생성할 경우, 이들은 같은 디스크 상의 리포지터리를 사용하게 되고, 테스트 환경의 오염으로 이어질 수 있습니다.

다른 파일들은 specs에 의해 매뉴얼으로 관리되어야 합니다. 예를 들어, tmp/test-file.csv 파일을 생성하는 코드를 실행한다면, 해당 spec은 정리의 일환으로 파일이 제거되도록 보장해야 합니다.

지속적인 인메모리 응용 프로그램 상태

주어진 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 인스턴스는 사양 간에 공유되지 않지만, 클래스모듈은 일반적으로 공유됩니다. Class 및 모듈 인스턴스 변수, 액세서, 클래스 변수 및 기타 상태 기반 언어 기능은 전역 변수와 마찬가지로 처리해야 합니다. 필요한 경우에만 수정하지 마십시오! 특히 수정이 필요한 경우 의존성 주입과 스텁과 함께 기대치 사용을 선호하십시오. 다른 선택지가 없는 경우 전역 변수 예제처럼 around 블록을 사용할 수 있지만 가능하면 이를 피하십시오.

Elasticsearch 사양

Elasticsearch를 필요로 하는 사양은 :elastic 특성으로 표시해야 합니다. 이렇게하면 모든 예제 전에 및 후에 색인을 생성하고 삭제합니다.

` :elastic_delete_by_query` 특성은 각 context 시작 및 끝에 색인을 생성하고 삭제하여 파이프라인의 실행 시간을 줄이기 위해 추가되었습니다. Elasticsearch delete by query API는 예제 간에(마이그레이션 색인 제외) 모든 색인에서 데이터를 삭제하는 데 사용됩니다.

` :elastic_clean 특성은 예제 간에 색인을 생성하고 삭제하여 깨끗한 색인을 보장합니다. 이렇게하면 테스트가 필수적이지 않은 데이터로 오염되지 않습니다. :elastic 또는 :elastic_delete_by_query 특성을 사용하면 문제가 발생하면 대신 :elastic_clean을 사용합니다. :elastic_clean`은 다른 특성보다 훨씬 느리며 절약해서 사용해야 합니다.

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

  • PostgreSQL에 데이터 생성하고 Elasticsearch에 색인 작업을 기다림.
  • 해당 데이터 검색.
  • 테스트가 예상 결과를 제공하는 지 확인.

개별 레코드 대신 구조적 변경을 확인하는 것과 같이 몇 가지 예외가 있습니다.

note
Elasticsearch 색인은 Gitlab::Redis::SharedState를 사용합니다. 따라서 Elasticsearch traits는 동적으로 :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 이벤트 테스트

caution
Snowplow는 contracts gem을 사용하여 런타임 유형 검사를 수행합니다. 기본적으로 테스트 및 개발에서 Snowplow가 비활성화되어 있으므로 Gitlab::Tracking을 모킹할 때 예외를 잡기가 어려울 수 있습니다.

유형 검사로 인한 런타임 오류를 잡기 위해 expect_snowplow_event를 사용할 수 있습니다. 이 함수는 Gitlab::Tracking#event 호출을 확인합니다.

describe '#show' do
  it '트랙하는 snowplow 이벤트' 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 '어떤 snowplow 이벤트도 트랙하지 않음' 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가 실패합니다.

스키마와 일치하는 Snowplow 컨텍스트 테스트

스노플로우 스키마 매처는 JSON 스키마에 대한 Snowplow 컨텍스트의 테스트를 통해 유효성 검사 오류를 줄이는 데 도움이됩니다. 스키마 매처는 다음 매개 변수를 허용합니다.

  • 스키마 경로
  • 컨텍스트

스키마 매처 사양을 추가하려면:

  1. Iglu 리포지터리에 새 스키마를 추가한 다음 동일한 스키마를 spec/fixtures/product_intelligence/ 디렉터리에 복사합니다.
  2. 복사한 스키마에서 "$schema" 키와 값을 제거합니다. 사양에 필요하지 않기 때문에 키를 유지하면 스펙이 실패합니다.
  3. 다음 코드를 사용하여 스키마 매처를 호출합니다.

    match_snowplow_context_schema(schema_path: '<step1의 파일명>', context: <컨텍스트 해시> )
    

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

이 스타일의 테스트는 포괄적인 입력 범위로 코드 한 조각을 실행하는 데 사용됩니다. 테스트 케이스를 한 번 지정하면 입력 값 및 각각에 대한 예상 출력이 있는 테이블과 함께 테스트를 읽기 쉽고 더 간결하게 만들 수 있습니다.

우리는 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를 포함해야 합니다.

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

Prometheus 테스트

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

Matchers

사용자 정의 matchers는 RSpec 예상의 의도를 명확하게 하거나/그 복잡성을 숨기기 위해 생성되어야 합니다. 이러한 matcher는 spec/support/matchers/ 아래에 배치해야 합니다. Matchers는 특정 유형의 스펙(예: 피처 또는 요청)에만 해당되는 경우 하위 폴더에 배치할 수 있지만, 여러 유형의 스펙에 적용되는 경우에는 배치하지 말아야 합니다.

be_like_time

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

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

다음 스니펫을 고려해보세요:

project = create(:project)

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

Linux에서 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 타임스탬프 유형과 일치하며 일치할 수 있습니다.

이 문제를 피하기 위해 be_like_time 또는 be_within을 사용하여 두 시간이 서로 1초 이내에 있는지 비교할 수 있습니다.

예제:

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_statushave_http_statusexpect(response.status).to 대신 사용하는 것을 권장하며, 전자는 상태가 일치하지 않을 때 응답 본문을 표시할 수 있습니다. 이는 일부 테스트가 깨지기 시작할 때 그 이유를 알고 싶을 때 매우 유용합니다.

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

206과 같은 숫자 표현 대신 :no_content와 같은 명명된 HTTP 상태를 선호하세요. 지원되는 상태 코드의 디렉터리를 참조하세요.

예제:

expect(response).to have_gitlab_http_status(:ok)

match_schemamatch_response_schema

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

match_response_schema요청 스펙으로부터의 응답 객체에서 사용하기 위한 편리한 matcher입니다.

예제:

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

# 'ee' 디렉터리에서 일치합니다
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 예제의 복잡성을 숨기기 위해 일부 메소드를 제공하는 모듈입니다. 다른 예제와 공유되지 않는 경우 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 그룹에 혼합되어 있습니다. 따라서 create(...) 대신에 FactoryBot.create(...)을 호출해야 합니다.
  • 특성을 이용하여 정의 및 사용을 정리합니다.
  • 팩토리를 정의할 때, 결과 레코드가 유효성 검사를 통과하는 데 필요하지 않은 속성은 정의하지 마십시오.
  • 팩토리에서 인스턴스를 인스턴스화할 때, 테스트에서 필요하지 않은 속성을 제공하지 마십시오.
  • 콜백에서 연관관계 설정에 대해 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 객체로 제한되어야 하는 것은 아닙니다. 예제 참조.
  • 팩토리와 그들의 특성은 스펙에서 검증된 유효한 객체를 생성해야 합니다.
  • 팩토리에서 skip_callback 사용을 피해야 합니다. 자세한 내용은 이슈 #247865를 참고하세요.

픽스처

모든 픽스처는 spec/fixtures/ 아래에 위치해야합니다.

리포지터리

Merge Request을 Merge하는 것과 같은 일부 기능을 테스트하려면 일정 상태의 Git 리포지터리가 테스트 환경에 있어야 합니다. GitLab은 특정 공통 사례에 대해 gitlab-test 리포지터리를 유지보수합니다. 프로젝트 팩토리에 :repository 특성을 사용하여 리포지터리의 사본을 사용할 수 있습니다.

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

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

let(:project) do
  create(
    :project, :custom_repo,
    files: {
      'README.md'       => '내용',
      'foo/bar/baz.txt' => '여기에 더 많은 내용'
    }
  )
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.include를 직접 spec/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

테스트 문서로 돌아가기