테스트 최적화 방법

테스트 설계

GitLab에서의 테스트는 처음부터 고려해야 할 중요한 부분입니다. 우리가 기능을 디자인하는 것과 마찬가지로 우리의 테스트 디자인을 고려하는 것이 중요합니다.

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

테스트 휴리스틱은 이 문제를 해결하는 데 도움이 될 수 있습니다. 이는 우리 코드에서 일반적인 버그들이 어떻게 나타나는지 간결하게 다루고 있습니다. 우리의 테스트를 디자인할 때, 알려진 테스트 휴리스틱을 검토하는 데 시간을 들이는 것이 도움이 됩니다. Handbook의 Test Engineering 섹션에서 유용한 휴리스틱을 찾을 수 있습니다.

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을 사용하려면 SPRING=1 bundle exec guard를 사용하세요.

일반적인 지침

  • 단일 최상위 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 인수를 훅에 제공하지 마세요. 왜냐하면 이것이 기본값이기 때문입니다.
  • beforeafter 훅에 대해 :all 대신 :context로 범위를 한정하는 것이 좋습니다.
  • evaluate_script("$('.js-foo').testSomething()")를 사용할 때(또는 execute_script), 주어진 요소에 작용하는 경우 요소가 실제로 존재하는지 확인하려면 먼저 Capybara 매처(예: find('.js-foo'))를 사용하세요.
  • 테스트의 일부를 격리시키려면 focus: true를 사용하세요.
  • 여러 개의 기대값이 있는 경우 :aggregate_failures를 사용하세요.
  • 빈 테스트 설명 블록의 경우 테스트가 자명한 경우 specify를 사용하세요.
  • 실제로 존재하지 않는 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 빌드 시간 및 고정된 비용에 직접적인 영향을 미칩니다. 우리는 철저하고 정확하며 빠른 테스트를 원합니다. 여기에서 품질과 속도를 유지하기 위해 사용할 수 있는 도구와 기술에 대한 정보를 찾을 수 있습니다.

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

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

  • 기능 스펙에서의 :js, 전체 자바스크립트 지원 브라우저를 실행합니다.
  • :clean_gitlab_redis_cache, 예제에 깨끗한 Redis 캐시를 제공합니다.
  • :request_store, 예제에 요청 리포지터리를 제공합니다.

우리는 테스트 의존성을 줄이고자 하며, 능력을 피하는 것은 설정에 필요한 양을 줄입니다.

:js는 특히 피해야 합니다. 이는 특성 테스트가 브라우저에서 JavaScript 반응성이 필요할 때만 사용해야 합니다(예: Vue.js 컴포넌트를 클릭하는 경우). 헤드리스 브라우저를 사용하는 것은 앱의 HTML 응답을 구문 분석하는 것보다 훨씬 더 느립니다.

프로파일링: 테스트가 시간을 보내는 위치 확인

rspec-stackprof를 사용하여 플레임 그래프를 생성할 수 있으며, 이를 통해 테스트가 시간을 보내는 위치를 보여줍니다.

이 젬은 JSON 보고서를 생성하는데, 이를 https://www.speedscope.app에 업로드하여 대화식 시각화를 제공합니다.

설치

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

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

# JSON 보고서를 speedscope.app에 업로드합니다
speedscope tmp/<당신의-json-보고서>.json
플레임그래프 해석 방법

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

  • 플레임그래프에는 여러 가지 뷰가 있습니다. 특히 많은 함수 호출이 있는 경우 Left Heavy가 유용합니다(예: 피처 스펙).
  • 확대 또는 축소할 수 있습니다! 내비게이션 문서를 참조하세요.
  • 느린 피처 테스트를 수행 중이라면 검색하여 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/[경로]/[대상]/[테스트].rb

일반적인 변경 사항은 create 대신 build 또는 build_stubbed를 사용하는 것입니다:

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

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

Factory Profiler는 팩토리를 통한 반복적인 데이터베이스 영속성을 식별하는 데 도움을 줄 수 있습니다.

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

# 플레임그래프로 시각화
FPROF=flamegraph bin/rspec spec/[경로]/[대상]/[테스트].rb

많은 수의 생성된 팩토리는 팩토리 연쇄가 원인일 수 있으며, 이는 팩토리가 연결을 만들고 다시 만들 때 발생합니다. 이는 total timetop-level time 숫자 간의 현저한 차이로 식별할 수 있습니다:

   total   top-level     total time      time per call      top-level time               name
     
     208           0        9.5812초            0.0461초             0.0000초          namespace
     208          76       37.4214초            0.1799초            13.8749초            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를 사용하게 되며, 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.6378초            0.1496초             4.5366초            project
       8           8        0.0477초            0.0477초             0.0477초          namespace
let에 대해 이야기해봅시다

테스트에서 객체를 생성하고 변수에 저장하는 다양한 방법이 있습니다. 효율성이 떨어지는 순서부터 가장 효율적인 순서까지 나열해보겠습니다:

  • let!는 각 예제가 실행되기 전에 객체를 생성합니다. 또한 매 예제마다 새 객체를 생성합니다. 객체를 명시적으로 참조하지 않고 각 예제가 시작할 때마다 깨끗한 객체를 만들어야 하는 경우에만 사용하세요.
  • let은 느리게 객체를 생성합니다. 객체가 호출될 때까지 생성되지 않습니다. 단순한 값에 대해서 let은 괜찮지만, 팩토리와 같은 데이터베이스 모델과 관련된 경우보다 효율적인 대체제가 있습니다.
  • let_it_be_with_refindlet_it_be_with_reload와 유사하게 작동하지만, 전자는 ActiveRecord::Base#find를 호출합니다, 후자보다 reload가 일반적으로 더 빠릅니다.
  • let_it_be_with_reload는 동일한 컨텍스트의 모든 예제에 대해 한 번 객체를 만듭니다. 그러나 각 예제가 실행된 후에 데이터베이스 변경 사항이 되돌아가고 object.reload가 호출되어 객체를 원래 상태로 복원합니다. 따라서 객체를 변경하기 원하는 경우 이 예제 전후에 상태가 유출될 수 있습니다. 이러한 경우에는 다른 모델을 통한 상태 유출이 발생할 수 있으며, 이런 경우에는 특히 몇 가지 예제만 존재할 때 선택하기 쉬울 수 있습니다.
  • 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_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 사용을 고려해보세요.

멤버 접근 수준 스텁 설정

팩토리 스텁을 위해 멤버 접근 수준을 스텁하기 위해 ProjectGroup와 같은 stub_member_access_level을 사용하세요:

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

it 'admin_project 능력을 허용합니다' do
  stub_member_access_level(project, maintainer: maintainer)
  
  expect(policy).to be_allowed(:admin_project)
end
note
test code가 계속 유지되는 경우 project_authorizationsMember 레코드를 유지하는 stub 도우미를 사용하지 마세요. 대신 Project#add_member 또는 Group#add_member를 사용하세요.

추가 프로파일링 지표

우리는 테스트를 실행할 때 발생하는 SQL 쿼리의 수와 같은 문제를 진단하는 데 rspec_profiling gem을 사용할 수 있습니다.

이것은 !123810와 같이 테스트에 의해 트리거된 일부 응용 프로그램 쪽 SQL 쿼리로 인한 것일 수 있습니다.

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

느린 기능 테스트 문제 해결

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

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 spec 실행를 참조하세요.

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

stackprof flamegraphs를 사용할 때 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

이 결과에서 우리는 테스트 중 가장 마찬가지의 예시를 볼 수 있으며, 시작할 수 있는 곳을 제공합니다. 여기서 가장 비싼 예시들은 공유된 예시(shared examples)에 있으며, 일반적으로 줄이는 것이 더 큰 영향을 미치므로 그들이 여러 곳에서 호출되기 때문에 일반적으로 줄임이 있습니다.

상위 느린 테스트

우리는 테스트 지속 시간에 대한 정보를 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 'frobulance를 설정합니다' 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?를 사용할 수 있습니다.

예: 스키마 유효성 검사기는 라이선스에 따라 다른 경로를 읽습니다

SaaS에 따라 다른 테스트 실행

코드를 테스트하는데 SaaS에서만 실행되는 코드를 테스트하려면 콘텍스트/스펙 블록에 :saas RSpec 메타데이터 태그 헬퍼를 사용할 수 있습니다. 이 헬퍼는 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와 같이 명명해야 합니다.
  • 성공 및 실패 경로를 설명하는 시나리오 제목을 사용하세요.
  • “성공적으로”와 같이 아무 정보도 추가하지 않는 시나리오 제목은 피하세요.
  • 기능 제목을 반복하는 시나리오 제목은 피하세요.
  • 데이터베이스에 필요한 레코드만 생성하세요
  • 행복한 경로와 조금 덜 행복한 경로를 테스트하세요.
  • 나머지 가능한 경로는 유닛 또는 통합 테스트로 테스트하세요.
  • 페이지에 표시된 내용을 테스트하세요. ActiveRecord 모델의 내부가 아닙니다. 예를 들어, 레코드가 생성되었는지 확인하려면 그 속성이 페이지에 표시되는지 예상을 추가하세요. Model.count가 하나 증가했다고 확인하는 것이 아니라요.
  • DOM 요소를 찾는 것은 괜찮지만 이것을 남발하지 마세요. 이렇게 하면 테스트가 더 취약해집니다.

UI 테스트

UI를 테스트 할 때, 사용자가 보는 것과 UI와 상호 작용하는 것을 모방하는 테스트를 작성하세요. 즉, Capybara의 의미 있는 방법을 선호하고 ID, 클래스 또는 속성으로 쿼리하는 것을 피하세요.

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

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

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

필요한 경우 within을 사용하여 페이지의 특정 영역 내에서 상호 작용을 제한할 수 있습니다.

axe 자동 접근성 테스트를 실행하기 위해 be_axe_clean 매처를 사용할 수 있습니다.

외부화된 내용

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"]')
Matchers

가능한 경우 아래와 같은 보다 구체적인 matchers를 사용하세요.

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

# 더 구체적인 matcher가 사용 불가능한 경우 허용
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
모달과 상호작용

GitLab UI 모달과 상호작용하기 위해 within_modal 헬퍼를 사용하세요.

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('Are you sure you want to delete this user?', button_text: 'Delete') 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
예제 실행을 재개하려면 아무 키나 누르세요!
예제로 돌아가기!
.

Finished in 34.51 seconds (files took 0.76702 seconds to load)
1 example, 0 failures

live_debug는 JavaScript가 활성화된 스펙에서만 작동합니다.

시각적 브라우저에서 :js 스펙 실행

다음과 같이 WEBDRIVER_HEADLESS=0로 spec를 실행하여 :js 스펙을 시각적으로 실행하세요:

WEBDRIVER_HEADLESS=0 bin/rspec some_spec.rb

테스트가 빠르게 완료되지만 상황을 파악할 수 있습니다. WEBDRIVER_HEADLESS=0을 사용하여 live_debug를 사용하면 열린 브라우저가 일시 중지되며 페이지를 다시 열지 않습니다. 디버그 및 엘리먼트 검사에 사용할 수 있습니다.

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

스크린샷

실패 시 자동으로 스크린샷을 촬영하는 capybara-screenshot 젬을 사용합니다. CI에서 이러한 파일을 작업 아티팩트로 다운로드할 수 있습니다.

또한 다음과 같은 메서드를 추가하여 테스트 중에 매뉴얼으로 스크린샷을 촬영할 수 있습니다. 더 이상 필요하지 않게 되면 반드시 제거하세요! 추가 방법은 https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots를 확인하세요.

:js 스펙에 screenshot_and_save_page를 추가하여 Capybara가 “보이는” 것을 스크린샷하고 페이지 소스를 저장합니다.

:js 스펙에 screenshot_and_open_image를 추가하여 Capybara가 “보이는” 것을 스크린샷하고 이미지를 자동으로 엽니다.

이로 생성된 HTML 덤프는 CSS가 없습니다. 이는 실제 응용 프로그램과 아주 다르게 보이게 됩니다. 더 쉬운 디버깅을 위해 작은 해킹으로 CSS를 추가합니다.

빠른 유닛 테스트

어떤 클래스는 Rails에서 완전히 격리되어 있습니다. 일반 spec_helper의 오버헤드 없이 이러한 클래스를 테스트할 수 있어야 합니다. 이러한 경우 테스트가 실행되는 파일에서 require 'fast_spec_helper'를 사용하여 대신 빠르게 실행될 수 있습니다.

  • 젬 로딩이 건너뛰어집니다
  • Rails 앱 부트를 건너뛸 수 있습니다
  • GitLab Shell 및 Gitaly 설정을 건너뛸 수 있습니다
  • 테스트 리포지터리 설정이 건너뛸 수 있습니다

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

특정 경우에는 여전히 몇몇 의존성을 require_dependency를 사용하여 로드해야 할 수도 있습니다. 예를 들어, Gitlab::UntrustedRegexp 클래스를 호출하는 코드를 테스트하려는 경우 해당 클래스가 바로 re2 라이브러리를 사용하므로:

  • lib/ 디렉터리에 있는 모든 ActiveSupport 확장을 로드합니다.
  • require_dependency를 사용하여 일부 의존성을 여전히 로드해야 하는 경우도 있음을 유의하세요.

유효한 분리되었는 지 확인하기 위해 코드 및 해당 스펙을 개별적으로 실행하고 fast_spec_helper를 사용하는 것이 좋습니다. spring rspec 사용을 지양하세요.

caution
fast_spec_helper를 사용하여 테스트를 빠르게 실행하는데 약 1초가 소요됩니다. 일반적인 spec_helper으로 실행할 때는 30초 이상 소요됩니다.

결론

테크니컬 마크다운 번역기를 사용하여 번역된 내용을 확인할 수 있습니다.

subjectlet 변수

GitLab RSpec 스위트는 중복을 줄이기 위해 let(엄격한, 비게으름 버전인 let!과 함께) 변수를 널리 사용했습니다. 그러나 때로는 이해하기 어려움을 야기하기 때문에 앞으로의 사용에 대한 일부 지침을 정해야 합니다.

  • let! 변수는 인스턴스 변수보다 선호됩니다. let 변수가 let! 변수보다 선호됩니다. 로컬 변수가 let 변수보다 선호됩니다.
  • let을 사용하여 전체 spec 파일 전체에서 중복을 줄입니다.
  • 하나의 테스트에서 사용되는 변수를 정의하기 위해 let을 사용하지 마세요. 대신, 테스트의 it 블록 내에서 로컬 변수로 정의하세요.
  • 단일 describe 블록 내에서만 사용되는 let 변수를 정의하지 마세요. 더 깊은 수준의 contextdescribe 블록에서만 사용되는 변수를 정의하지 마세요. 사용되는 곳과 가능한 가까이 정의하세요.
  • 하나의 let 변수의 정의를 다른 것으로 덮어쓰지 마세요.
  • 다른 것의 정의에만 사용되는 let 변수를 정의하지 마세요. 대신 도우미 메서드를 사용하세요.
  • let! 변수는 엄격한 순서 지정을 필요로 할 때에만 사용되어야 합니다. 그렇지 않으면 let이 적당합니다. let은 게으르기 때문에 참조될 때까지 계산되지 않습니다.
  • 예제에서 subject를 참조하지 마세요. 대신 명명된 subject subject(:name) 또는 let 변수를 사용하여 변수에 컨텍스트 이름을 부여하세요.
  • 예제에서 subject를 참조하지 않는 경우, 이름이 지정되지 않은 상태로 subject를 정의하는 것이 허용됩니다.

공통 테스트 설정

note
let_it_bebefore_all은 DatabaseCleaner의 삭제 전략과 호환되지 않습니다. 이는 마이그레이션 특정사항, Rake 작업 특정사항 및 :delete RSpec 메타데이터 태그가 있는 특정사항을 포함합니다. 더 많은 정보는 이슈 420379를 참조하세요.

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

가능한 한 before(:all) 또는 before(:context)를 사용하여 이를 구현하지 마십시오. 그렇게 하면 데이터 정리가 데이터베이스 트랜잭션 외부에서 실행될 수 있으므로 매뉴얼으로 데이터를 정리해야 합니다.

대신, let_it_be 변수와 before_all 훅을 test-prof gem에서 사용하여 이를 구현할 수 있습니다.

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_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와 같은 stubs가 있는 팩토리와 함께 사용할 수 없습니다. 그 이유는 let_it_bebefore(:all) 블록에서 발생하며, RSpec이 before(:all)에서 stub을 허용하지 않기 때문입니다. 자세한 내용은 이슈를 참조하세요. 해결하기 위해 let을 사용하거나 팩토리를 stub을 사용하지 않도록 변경하세요.

시간에 민감한 테스트

ActiveSupport::Testing::TimeHelpers 를 사용하여 시간에 민감한 사항을 확인할 수 있습니다. 시간에 민감한 것을 테스트하거나 확인하는 경우에는 이러한 도우미를 사용하여 일시적인 테스트 실패를 방지하세요.

예시:

it 'is overdue' do
  issue = build(:issue, due_date: Date.tomorrow)
  
  travel_to(3.days.from_now) do
    expect(issue).to be_overdue
  end
end

RSpec 도우미

RSpec 메타데이터 태그 도우미 :freeze_time:time_travel_to를 사용하여 ActiveSupport::Testing::TimeHelpers 메서드를 전체 spec을 감싸기 위해 필요한 부풀린 코드의 양을 줄일 수 있습니다.

describe 'specs which require time to be frozen', :freeze_time do
  it 'freezes time' do
    right_now = Time.now
    
    expect(Time.now).to eq(right_now)
  end
end

describe 'specs which require time to be frozen to a specific date and/or time', time_travel_to: '2020-02-02 10:30:45 -0700' do
  it 'freezes time to the specified date and time' do
    expect(Time.now).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
  end
end

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

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

타임스탬프 절단

액티브 레코드 타임스탬프는 Rails의 ActiveRecord::Timestamp 모듈에 의해 사용 Time.now됩니다. 시간 정밀도는 OS에 따라 다릅니다. 문서에 명시된대로 소수 초를 포함할 수 있습니다.

Rails 모델이 데이터베이스에 저장될 때, 타임스탬프는 PostgreSQL에서 timestamp without time zone라는 유형을 사용하여 저장됩니다. 이는 마이크로초 해상도(즉, 소수점 이하 6자릿수)를 가지고 있습니다. 따라서 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 파일 쌍의 매뉴얼 이진 분할)을 사용하여 어떤 spec이 잘못되었는지를 확인할 수 있습니다. 문제를 해결하려면 테스트 스위트가 환경을 원본 상태로 유지하는 방식에 대한 이해가 필요합니다. 각 데이터 리포지터리에 대해 자세히 알아보세요!

SQL 데이터베이스

이 부분은 database_cleaner 젬에 의해 우리를 위해 관리됩니다. 각 spec은 트랜잭션으로 둘러싸여 있으며, 테스트가 완료된 후에 롤백됩니다. 일부 spec은 대신 완료 후에 모든 테이블에 대해 DELETE FROM 쿼리를 실행합니다. 이것은 생성된 행을 여러 데이터베이스 연결에서 볼 수 있도록 합니다. 이것은 브라우저에서 실행되는 spec이나 기타 상황에서 중요합니다.

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

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

마이그레이션 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 특성 중 하나로 스스로를 표시해야 합니다.

백그라운드 작업 / Sidekiq

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

Sidekiq 인라인 모드가 페이크 모드로 변경되면서, :sidekiq_might_not_need_inline 특성이 추가되었습니다. 이 특성은 실제로 작업을 처리하기를 예상하는 테스트에 모두 추가됩니다. 이 특성은 리팩토링되어야 하거나 :sidekiq_might_not_need_inline 특성을 :sidekiq_inline로 업데이트해야 하는 테스트 중 하나입니다.

perform_enqueued_jobs 사용은 우리의 Sidekiq 워커가 ApplicationJob/ActiveJob::Base에서 상속되지 않기 때문에 지연 메일 전달을 테스트하는 데에만 유용합니다.

DNS

로컬 네트워크에 따라 DNS가 문제를 일으킬 수 있기 때문에 !22368부터 전체적으로 DNS 요청을 스텁핑합니다. spec/support/dns.rb에 있는 RSpec 레이블을 적용하여 DNS 스텁핑을 우회해야 하는 경우, 다음과 같이 할 수 있습니다:

it "really connects to Prometheus", :permit_dns do

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

속도 제한

속도 제한은 테스트 스위트에서 활성화되어 있습니다. :js 특성을 사용하는 기능 spec에서 속도 제한이 트리거될 수 있습니다. 대부분의 경우에는 속도 제한을 트리거하는 것을 피하기 위해 :clean_gitlab_redis_rate_limiting 특성을 테스트에 표시할 수 있습니다. 이 특성은 spec 간에 Redis 캐시에 저장된 속도 제한 데이터를 지웁니다. 단일 테스트가 속도 제한을 트리거하는 경우, 대신 :disable_rate_limit을 사용할 수 있습니다.

파일 메서드 스텁화

파일의 내용을 스텁화해야 하는 상황에서는, stub_file_readexpect_file_read 도우미 메서드를 사용하세요. 이러한 메서드는 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: "가짜 파일 내용")

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

파일 시스템

파일 시스템 데이터는 “리포지터리”와 “기타 모든 것”으로 대략적으로 나눌 수 있습니다. 리포지터리는 tmp/tests/repositories에 저장됩니다. 이 디렉터리는 테스트 실행 전과 후에 비워집니다. 그러나 spec 간에 비워지지는 않으므로, 생성된 리포지터리들이 이 디렉터리에 테스트 실행 중에 축적될 수 있습니다. 이들을 삭제하는 것은 비용이 많이 듭니다. 그러나 신중한 관리 없이는 오염될 수 있습니다.

이를 피하기 위해 테스트 스위트에 해시 리포지터리가 활성화되어 있습니다. 이는 리포지터리가 해당 프로젝트의 ID에 따라 고유한 경로를 가지도록 하는 것을 의미합니다. 프로젝트 ID가 spec 간에 재설정되지 않기 때문에, 각 spec은 디스크 상에 고유한 리포지터리를 가지고 있으며, 변경 사항이 spec 간에 보이지 않게 됩니다.

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

업로드와 같은 데이터베이스 객체에 연결된 다른 파일 클래스는 일반적으로 동일한 방식으로 관리됩니다. 테스트에서 해시 리포지터리가 활성화되어 있기 때문에, 이 파일들은 ID에 따라 디스크에 작성되므로 충돌이 발생하지 않아야 합니다.

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

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

In-memory application state

특정 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에 색인되길 기다립니다. - 해당 데이터를 검색합니다. - 테스트가 예상한 결과를 내는지 확인합니다.

개별 레코드 대신 구조적 변경을 확인하는 등 일부 예외가 있습니다.

note
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! 메서드를 사용할 수 있습니다. 이는 데이터가 PostgreSQL에 로드된 후에 데이터가 색인화되어 검색 가능하도록 하기 위해 Elasticsearch Refresh API를 사용합니다.

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 '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는 인자가 지정되지 않은 경우에도 실행되면 실패할 수 있습니다.

Snowplow 컨텍스트에 대한 스키마 테스트

Snowplow 스키마 매처는 JSON 스키마에 대한 Snowplow 컨텍스트를 테스트하여 유효성 오류를 줄입니다. 스키마 매처는 다음 매개변수를 허용합니다:

  • 스키마 경로
  • 컨텍스트

스키마 매처 스펙을 추가하려면 다음을 수행합니다:

  1. Iglu 리포지터리에 새 스키마를 추가한 다음 동일한 스키마를 spec/fixtures/product_intelligence/ 디렉터리에 복사합니다.
  2. 복사된 스키마에서 "$schema" 키와 값을 제거합니다. 테스트용이기 때문에 스키마가 URL에서 찾으려고 하면 스펙이 실패합니다.
  3. 스키마 매처를 호출하기 위해 다음 스니펫을 사용합니다:

    match_snowplow_context_schema(schema_path: '<단계 1에서 받은 파일 이름>', 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 블록에서는 간단한 값만 입력으로 사용해야 합니다. proc, 상태를 가지는 객체, FactoryBot로 생성된 객체 등을 사용하면 예상치 못한 결과가 발생할 수 있습니다.

프로메테우스 테스트

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

매처

사용자 정의 매처는 RSpec 기대치의 의도를 명확히 하거나 복잡성을 숨기기 위해 만들어져야 합니다. 이들은 spec/support/matchers/ 아래에 배치되어야 합니다. 매처는 특정 유형의 스펙(예: 피처 또는 리퀘스트)에만 적용되는 경우 해당 하위 폴더에 배치되어야 하지만, 여러 유형의 스펙에 적용되는 경우에는 그렇게 되어서는 안 됩니다.

be_like_time

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

포스트그리SQL의 시간 및 타임스탬프 유형은 마이크로초의 해상도를 갖습니다. 그러나 루비 Time의 정밀도는 OS에 따라 달라질 수 있습니다.

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

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의 정밀도가 포스트그리SQL의 타임스탬프 유형과 일치하며 일치할 수 있습니다.

이 문제를 피하기 위해, 시간이 서로 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_statushave_http_statusexpect(response.status).to보다 선호하며, 전자는 상태 불일치시 응답 본문을 보여줄 수 있습니다. 이는 어떤 테스트가 실패하는 이유를 알고 싶을 때 매우 유용합니다. 소스를 편집하고 테스트를 다시 실행하지 않고도 결과를 확인할 수 있습니다.

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

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

예시:

expect(response).to have_gitlab_http_status(:ok)

match_schemamatch_response_schema

match_schema 매처를 사용하면 주도디렉터리(JSON Schema)과 일치하는지 확인할 수 있습니다. 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으로 구문 분석되고 비어 있지 않은 결과를 반환하는지 확인할 수 있습니다. 이를 위해 위에서 언급한 스키마 일치와 결합할 수 있습니다.

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/support/shared_contexts/ 아래에 배치되어야 합니다.
  • 특정 유형의 스펙(예: 피처 또는 리퀘스트)에만 적용되는 경우 해당 하위 폴더에 배치되어야 하지만, 여러 유형의 스펙에 적용되는 경우에는 그렇게 되어서는 안 됩니다.

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

공유 예제

한 개의 스펙 파일에서만 사용되는 공유 예제는 인라인으로 선언할 수 있습니다. 한 개 이상의 스펙 파일에서 사용되는 공유 예제는 다음과 같이 처리해야 합니다.

  • spec/support/shared_examples/ 아래에 배치되어야 합니다.
  • 특정 유형의 스펙(예: 피처 또는 리퀘스트)에만 적용되는 경우 해당 하위 폴더에 배치되어야 하지만, 여러 유형의 스펙에 적용되는 경우에는 그렇게 되어서는 안 됩니다.

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

헬퍼

헬퍼는 특정 RSpec 예제의 복잡성을 숨기기 위해 메서드를 제공하는 모듈입니다. 다른 스펙과 공유되지 않는다면 RSpec 파일에 정의할 수 있습니다. 그렇지 않은 경우 spec/support/helpers/에 배치되어야 합니다. 헬퍼는 특정 유형의 스펙(예: 피처 또는 리퀘스트)에만 적용되는 경우 해당 하위 폴더에 배치되어야 하지만, 여러 유형의 스펙에 적용되는 경우에는 그렇게 되어서는 안 됩니다.

헬퍼는 루비 온레임 명명/네임스페이싱 규칙을 따라야 하며, spec/support/helpers/features/iteration_helpers.rb와 같이 명확한 이름을 가져야 합니다.

헬퍼는 RSpec 구성을 변경해서는 안 됩니다. 위에서 언급한 헬퍼 모듈에는 다음과 같은 내용이 포함되어서는 안 됩니다.

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

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

루비 상수 테스트

루비 상수를 사용하는 코드를 테스트할 때는 상수의 값보다는 해당 상수에 의존하는 동작에 집중해야 합니다.

예를 들어, 다음은 .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

상수의 오류가 치명적인 영향을 미칠 수 있는 경우에는 상수 값 테스트가 추가적인 안전장치로 유용할 수 있습니다. 예를 들어, 전체 GitLab 서비스를 다운시킬 수 있거나 고객에게 예상치 않은 과금을 할 수 있는 경우나 우주를 붕괴시킬 수 있는 경우는 해당합니다.

팩토리

GitLab은 테스트 fixture 대체로 factory_bot을 사용합니다.

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

    has_manybelongs_to association을 사용하여 Factory를 생성할 때, 빌드되는 객체를 참조하기 위해 instance 메서드를 사용하세요. 이를 통해 상호 연결된 associations으로 인한 불필요한 레코드 생성이 방지됩니다.

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

    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
    
  • Factory는 ActiveRecord 객체로 제한될 필요가 없습니다. 예제를 확인하세요.
  • Factory 및 해당 traits는 공유된 specs에 의해 검증된 유효한 객체를 생성해야 합니다.
  • Factory에서 skip_callback 사용을 피하세요. 자세한 내용은 이슈 #247865를 참조하세요.

픽스처

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

리포지터리

Merge Request을 Merge하는 등의 기능을 테스트하려면 특정 상태의 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와 같이 특정 도메인과 관련이 있어야 합니다.

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

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/fast_spec_helper.rb 파일에서 사용되는 spec/support/rspec.rb 파일에 포함하는 것을 고려하세요. spec/fast_spec_helper.rb 파일에 대한 자세한 내용은 빠른 유닛 테스트를 참조하세요.

테스트 환경 로깅

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

==> Setting up Gitaly...
    Gitaly set up in 31.459649 seconds...

==> Setting up GitLab Workhorse...
    GitLab Workhorse set up in 29.695619 seconds...
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

==> Setting up GitLab Elasticsearch Indexer...
    GitLab Elasticsearch Indexer set up in 26.514623 seconds...

로컬에서 실행되지 않거나 아무 작업이 필요하지 않은 경우에는 이 정보가 생략됩니다. 이러한 메시지를 항상 보려면 다음과 같은 환경 변수를 설정하세요.

GITLAB_TESTING_LOG_LEVEL=debug

테스트 문서로 돌아가기