- 테스트 디자인
-
RSpec
- 일반 가이드라인
- 응용 프로그램 코드의 즉시 로딩
- 루비 경고
- 테스트 순서
- 테스트 불안정성
- 테스트 지연
- 기능 카테고리 메타데이터
- EE 라이선스에 따라 테스트
- SaaS에 따라 테스트
- 커버리지
- 시스템/기능 테스트
- 빠른 단위 테스트
subject
및let
변수- 공통 테스트 설정
- 시간에 민감한 테스트
- 테스트에서 기능 플래그
- 원점 테스트 환경
- 테이블 기반 / 매개변수화된 테스트
- Prometheus 테스트
- Matchers
- 쿼리 성능 테스트
- 공유된 컨텍스트
- 공유된 예시
- 헬퍼(도우미)
- Ruby 상수 테스트
- 팩토리
- 픽스처
- 저장소
- 구성
- 테스트 환경 로깅
테스팅 최적의 방법
테스트 디자인
GitLab에서의 테스팅은 1급 시민으로, 막바지에 덧붙이는 것이 아닙니다. 우리는 우리의 기능을 디자인하는 것처럼 테스트의 디자인을 고려하는 것이 중요합니다.
기능을 구현할 때, 우리는 올바른 능력을 올바른 방식으로 개발하는 데에 대해 고려합니다. 이것은 우리가 관리 가능한 수준으로 우리의 범위를 좁히는 데에 도움이 됩니다. 기능에 대한 테스트를 구현할 때, 올바른 테스트를 개발하는 것 뿐만 아니라 테스트가 실패할 수 있는 중요한 방법을 모두 다루어야 합니다. 이는 우리의 범위를 빠르게 관리하기 어려운 수준까지 확장시킬 수 있습니다.
테스트 휴리스틱은 이 문제를 해결하는 데 도움이 될 수 있습니다. 그것들은 우리 코드에서 일반적으로 버그가 나타나는 다양한 방법을 간결하게 다룹니다. 우리의 테스트를 디자인할 때, 알려진 테스트 휴리스틱을 검토하는 데 시간을 내어 우리의 테스트 디자인에 도움을 줄 수 있습니다. Handbook의 Test Engineering 섹션에서 유용한 휴리스틱을 찾을 수 있습니다.
RSpec
RSpec 테스트를 실행하려면:
# 파일에 대한 테스트 실행
bin/rspec spec/models/project_spec.rb
# 파일에서 10번째 라인 예시에 대한 테스트 실행
bin/rspec spec/models/project_spec.rb:10
# 해당 문자열(associations)을 포함하는 예시 이름의 테스트 실행
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
를 사용하세요. - 테스트에서 리터럴 URL에 대한 단언을 하는 경우,
example.com
,gitlab.example.com
을 사용하세요. 이렇게 함으로써 실제 URL을 사용하지 않도록 할 수 있습니다. - 순차 생성된 속성의 절대 값에 대한 단언하지 마세요(참조: Gotchas).
-
expect_any_instance_of
또는allow_any_instance_of
를 사용하지 마세요(참조: Gotchas). - 기본값이기 때문에
:each
인자를 훅에 제공하지 마세요. -
before
와after
훅에서:all
대신:context
에 대해 범위가 있는 것을 선호하세요. - 특정 요소에 작용하는
evaluate_script("$('.js-foo').testSomething()")
(또는execute_script
)를 사용할 때, 실제로 요소가 존재하는지 확인하려면 먼저 Capybara matcher(예: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
태그를 추가하세요. 이렇게 하면 테스트 실행 전에 응용 프로그램 코드가 즉시 로드됨을 보장할 수 있습니다.
루비 경고
우리는 기본적으로 스펙을 실행할 때 사양에서 deprecation 경고를 활성화시켰습니다. 이러한 경고를 개발자에게 더 눈에 띄게 만드는 것은 더 높은 버전의 루비로 업그레이드하는 데 도움이 됩니다.
예를 들어, 환경 변수 SILENCE_DEPRECATIONS
를 설정하여 deprecation 경고를 차단할 수 있습니다:
# 모든 deprecation 경고를 차단
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
, 전체 JavaScript 지원 브라우저를 실행합니다. -
:clean_gitlab_redis_cache
, 예제에 깨끗한 Redis 캐시를 제공합니다. -
:request_store
, 예제에 요청 저장소를 제공합니다.
우리는 테스트 의존성을 줄이고 능력을 피하는 것도 줄이는 것입니다.
:js
를 피하는 것이 특히 중요합니다. 이것은 브라우저의 JavaScript 반응성이 필요한 경우에만 사용해야 합니다(예: Vue.js 구성 요소를 클릭하는 경우). 헤드리스 브라우저를 사용하면 앱의 HTML 응답을 파싱하는 것보다 훨씬 느립니다.
프로파일링: 테스트에서 시간을 소비하는 위치 파악하기
rspec-stackprof
를 사용하여 테스트에서 시간을 소비하는 위치를 보여주는 플레임 그래프를 생성할 수 있습니다.
이 gem은 JSON 보고서를 생성하며, 해당 보고서를 https://www.speedscope.app에 업로드하여 대화형 시각화를 볼 수 있습니다.
설치
stackprof
gem은 GitLab에 이미 설치되어 있습니다, 또한 JSON 보고서를 생성하는 스크립트(bin/rspec-stackprof
)도 사용할 수 있습니다.
# 선택 사항: `speedscope` 패키지를 설치하여 JSON 보고서를 https://www.speedscope.app에 쉽게 업로드합니다
npm install -g speedscope
JSON 보고서 생성
bin/rspec-stackprof --speedscope=true <your_slow_spec>
# 스크립트가 종료될 때 보고서의 이름이 표시됩니다.
# JSON 보고서를 speedscope.app에 업로드합니다
speedscope tmp/<your-json-report>.json
플레임그래프 해석 방법
다음은 플레임그래프를 해석하고 탐색하는 유용한 팁입니다.
- 플레임그래프에는 여러 가지 뷰가 있습니다. 많은 함수 호출이 있는 경우 ‘Left Heavy’가 특히 유용합니다(예: 특징 명세).
- 확대 또는 축소할 수 있습니다! 탐색 문서를 참조하세요.
- 느린 특징 테스트를 진행 중이라면 ‘Capybara::DSL#’을 검색하여 만든 Capybara 작업 및 수행시간을 확인할 수 있습니다!
분석 예제에 대한 자세한 내용은 #414929 또는 #375004를 참조하세요.
팩터리 사용 최적화
느린 테스트의 일반적인 원인은 객체의 과도한 생성 및 이로 인한 계산 및 DB 시간입니다. 팩터리는 개발에 필수적이지만 데이터베이스에 데이터를 삽입하는 것이 너무 쉬워 최적화할 수 있는지 확인할 수 있습니다.
여기서 주의할 두 가지 기본 기술은 다음과 같습니다.
- 줄이기: 객체 생성 및 영속성 부여를 피하십시오.
- 재사용: 특히 우리가 검토하지 않는 중첩된 공유된 객체들을 재사용할 수 있습니다.
생성을 피하기 위해서는 다음 사항을 고려할 가치가 있습니다.
-
instance_double
및spy
가FactoryBot.build(...)
보다 빠릅니다. -
FactoryBot.build(...)
및.build_stubbed
가.create
보다 빠릅니다. - DB 영속성은 느립니다!
데이터베이스 영속성이 필요하지 않은 경우를 찾기 위해 Factory Doctor를 사용해보세요.
# 경로에 대한 테스트 실행
FDOC=1 bin/rspec spec/[path]/[to]/[spec].rb
create
대신 build
또는 build_stubbed
를 사용하는 일반적인 변경 사항은 다음과 같습니다.
# 이전
let(:project) { create(:project) }
# 새로운 코드
let(:project) { build(:project) }
반복적인 데이터베이스 영속성을 식별하는 데 Factory Profiler를 사용할 수 있습니다.
# 경로에 대한 테스트 실행
FPROF=1 bin/rspec spec/[path]/[to]/[spec].rb
# 플레임그래프로 시각화하려면
FPROF=flamegraph bin/rspec spec/[path]/[to]/[spec].rb
큰 수의 생성된 팩터리는 팩터리 카스케이드의 일반적인 원인입니다. 이는 팩터리가 관계를 만들고 만들고 만들어 낼 때 발생합니다. 이러한 팩터리 카스케이드는 total time
과 top-level time
수치의 차이가 두드러집니다.
이 예제에는 명시적으로 생성하는 namespace
객체가 없음을 나타내는 표가 있습니다(top-level == 0
). 그러나 우리에게는 208개의 namespace
객체가 있습니다(각 프로젝트에 하나씩) 이것은 9.5초가 걸립니다.
명명된 팩터리의 모든 호출에서 단일 객체를 재사용하기 위해 FactoryDefault
를 사용할 수 있습니다.
FactoryDefault
를 사용하려면 let_it_be
로 생성한 객체가 있다면 모든 예제에 대해 기본 팩터리를 유지하도록 factory_default: :keep
을 명시적으로 지정해야 합니다. 이것은 각 예제를 위해 기본 팩터리를 재생성하는 대신 해당 기본 팩터리를 유지합니다.
무작위적인 의존성을 방지하기 위해 create_default
로 생성된 객체는 동결 상태입니다.
아마도 208개의 다른 프로젝트를 만들 필요가 없을 수 있습니다. 하나를 만들고 재사용할 수 있습니다. 또한 만든 프로젝트 중 약 1/3만 우리가 요청한 프로젝트입니다. 프로젝트에 대한 기본값 설정에 혜택이 있습니다.
let_it_be(:project) { create_default(:project) }
이 경우 total time
및 top-level time
수치가 더 일치합니다.
total top-level total time time per call top-level time name
31 30 4.6378s 0.1496s 4.5366s project
8 8 0.0477s 0.0477s 0.0477s namespace
let
에 대해 이야기해 봅시다
테스트에서 객체를 생성하고 변수에 저장하는 다양한 방법이 있습니다. 효율이 가장 낮은 것부터 가장 효율적인 것까지입니다:
-
let!
은 각 예제가 실행되기 전에 객체를 생성합니다. 또한 각 예제마다 새 객체를 생성합니다. 객체를 명시적으로 참조하지 않고 각 예제가 실행되기 전에 깨끗한 객체를 만들어야 하는 경우에만 이 옵션을 사용해야 합니다. -
let
은 객체를 지연해서 생성합니다. 객체가 호출될 때까지 만들어지지 않습니다.let
은 각 예제마다 새 객체를 만드는 것으로 일반적으로 비효율적입니다.let
은 간단한 값을 위해 괜찮지만, 데이터베이스 모델인 팩토리와 같은 것들을 다룰 때는 보다 효율적인 변형이 더 좋습니다. -
let_it_be_with_refind
는let_it_be_with_reload
와 유사하게 작동하지만 이전은ActiveRecord::Base#reload
대신에ActiveRecord::Base#find
를 호출합니다.reload
가 일반적으로refind
보다 빠릅니다. -
let_it_be_with_reload
는 동일한 컨텍스트 내의 모든 예제에 대해 한 번 객체를 생성하지만 각 예제 이후에는 데이터베이스 변경 사항이 롤백되고object.reload
가 호출되어 객체를 원래 상태로 복원합니다. 이는 예제 내에서 또는 중간에 객체를 변경할 수 있음을 의미합니다. 그러나 다른 모델들 사이로 상태가 누출될 수 있습니다 경우가 있습니다. 이러한 경우에는 몇 개의 예제만 있는 경우 특히let
이 더 쉬운 옵션일 수 있습니다. -
let_it_be
는 동일한 컨텍스트의 모든 예제에 대해 한 번 객체를 생성합니다. 이는let
및let!
에 대한 훌륭한 대안이며 다른 예제로부터 변경되지 않아야 하는 객체에 대해 드라마틱하게 테스트를 가속화할 수 있습니다. 자세한 내용 및 예제는 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_reload
가 let
보다 효율적인 경우에 대한 예는 다음과 같습니다:
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project) } # `let_it_be`를 사용하면 테스트가 실패합니다
'개발자가 있는' 컨텍스트에서
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
'관리자가 있는' 컨텍스트에서
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
팩토리 내에서 메서드 스텁 만들기
팩토리에서 allow(object).to receive(:method)
를 사용하는 것은 let_it_be
와 함께 사용할 수 없으므로 공통 테스트 설정에 설명된 대로 피해야 합니다.
대신 메서드를 스터브하기 위해 stub_method
를 사용할 수 있습니다:
before(:create) do |user, evaluator|
# 메서드를 스텁합니다.
stub_method(user, :some_method) { 'stubbed!' }
# 이름포함 매개변수를 포함하여 인수로 사용
stub_method(user, :some_method) { |var1| "Returning #{var1}!" }
stub_method(user, :some_method) { |var1: 'default'| "Returning #{var1}!" }
end
# 메서드의 스텁을 제거합니다.
# 이것은 스텁된 객체가 `let_it_be`로 생성되고 테스트 사이에 메서드를 재설정하려는 경우 유용할 수 있습니다
after(:create) do |user, evaluator|
restore_original_method(user, :some_method)
# 또는
restore_original_methods(user)
end
참고:
stub_method
는 let_it_be_with_refind
와 함께 사용할 때 동작하지 않습니다. 이는 stub_method
가 인스턴스의 메서드를 스텁하고 let_it_be_with_refind
가 각 실행마다 새로운 객체를 생성하기 때문입니다.
stub_method
는 메서드 존재 및 메서드 아리티 확인을 지원하지 않습니다.
경고:
stub_method
는 팩토리에서만 사용되어야 합니다. 다른 곳에서 사용하는 것은 권장되지 않습니다. 가능하다면 RSpec 모의객체를 사용하는 것을 고려해 보세요.
Member access level의 스텁
Project
또는 Group
과 같은 factory 스텁의 경우 스텁 멤버 액세스 레벨을 사용하십시오.
stub_member_access_level
을 사용하십시오:
let(:project) { build_stubbed(:project) }
let(:maintainer) { build_stubbed(:user) }
let(:policy) { ProjectPolicy.new(maintainer, project) }
it 'allows admin_project ability' do
stub_member_access_level(project, maintainer: maintainer)
expect(policy).to be_allowed(:admin_project)
end
참고:
테스트 코드가 project_authorizations
또는 Member
레코드를 유지하는 데 의존하는 경우 이 스텁 도우미를 사용하지 마십시오. 대신 Project#add_member
나 Group#add_member
를 사용하십시오.
추가 프로파일링 메트릭
rspec_profiling
젬을 사용하여, 예를 들어, 테스트를 실행할 때 만드는 SQL 쿼리의 수 등을 진단할 수 있습니다.
이는 테스트가 모의 테스트 중인 부분을 모의화할 수 있는 SQL 쿼리로 인한 일 수 있습니다.(예: 123810와 같이).
성능 문서의 지침을 참조하세요.
느린 기능 테스트 문제 해결
느린 기능 테스트는 일반적으로 다른 테스트와 동일한 방식으로 최적화될 수 있습니다. 그러나 문제 해결 세션을 더 유익하게 만들 수 있는 몇 가지 구체적 기술이 있습니다.
UI에서 기능 테스트가 하는 작업 보기
# Before
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
# After
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
추가 정보는 가시적 브라우저에서 :js
스펙 실행을 참조하세요.
프로파일링에 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
Finished in 2 minutes 19 seconds (files took 1 minute 4.42 seconds to load)
277 examples, 0 failures, 1 pending
이 결과를 통해 스펙의 가장 비싼 예제를 볼 수 있으며, 이를 토대로 시작할 수 있습니다. 여기서 가장 비싼 예제들은 공유 예제에 있으며, 이러한 예제에서의 감소는 일반적으로 여러 곳에서 호출되기 때문에 더 큰 영향을 미칩니다.
비싼 작업 반복 피하기
격리된 예제는 매우 명확하고 스펙의 명세를 위한 목적을 제공하지만, 다음 예제는 다른 단언을 위해 같은 작업을 반복하는 방법을 보여줍니다:
subject { described_class.new(arg_0, arg_1) }
it 'creates an event' do
expect { subject.execute }.to change(Event, :count).by(1)
end
it 'sets the frobulance' do
expect { subject.execute }.to change { arg_0.reset.frobulance }.to('wibble')
end
it 'schedules a background job' 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 예제에 대한 기능 카테고리 메타데이터를 설정해야 합니다.(feature_categorization/index.md 참조)
EE 라이선스에 따라 테스트
FOSS_ONLY=1
로 실행 여부에 따라 테스트를 실행하는 컨텍스트/스펙 블록에서 if: Gitlab.ee?
또는 unless: Gitlab.ee?
을 사용할 수 있습니다.
예: SchemaValidator는 라이선스에 따라 다른 경로를 읽음
SaaS에 따라 테스트
컨텍스트/스펙 블록에서 :saas
RSpec 메타데이터 태그 도우미를 사용하여 GitLab.com에서만 실행되는 코드를 테스트할 수 있습니다. 이 도우미는 Gitlab.config.gitlab['url']
을 Gitlab::Saas.com_url
로 설정합니다.
커버리지
simplecov
는 코드 테스트 커버리지 보고서를 생성하는 데 사용됩니다. 이러한 보고서는 CI에서 자동으로 생성되지만 로컬에서 테스트를 실행할 때 부분 보고서를 생성하려면 SIMPLECOV
환경 변수를 설정하십시오:
SIMPLECOV=1 bundle exec rspec spec/models/repository_spec.rb
커버리지 보고서는 앱 루트의 coverage
폴더에 생성되며 다음과 같은 방법으로 열 수 있습니다.
firefox coverage/index.html
커버리지 보고서를 사용하여 테스트가 코드의 100%를 커버하도록 보장합니다.
시스템/기능 테스트
참고: 새로운 시스템 테스트를 작성하기 전에 전체 가이드를 고려하십시오.
- 기능 스펙은
ROLE_ACTION_spec.rb
와 같이 명명해야 합니다.user_changes_password_spec.rb
와 같이. - 성공 및 실패 경로를 설명하는 시나리오 제목을 사용하세요.
- “성공적으로”와 같은 정보를 추가하지 않는 시나리오 제목을 피하세요.
- 기능 제목을 반복하지 않도록 시나리오 제목을 피하세요.
- 데이터베이스에 필요한 레코드만 만드세요.
- 행복한 경로와 불행한 경로를 테스트합니다.
- 나머지 가능한 경로는 단위 또는 통합 테스트로 테스트하세요.
- 페이지에 표시된 내용을 테스트하세요. 예를 들어, 레코드가 만들어졌다는 것을 확인하려면 그 속성이 페이지에 표시되는 것을 기대하세요.
Model.count
가 1 증가했다고 하여 테스트하지 말고요. - DOM 요소를 찾는 것은 괜찮지만, 테스트를 더 연약하게 만드므로 남용하지 마세요.
UI 테스트
UI를 테스트할 때, 사용자가 보는 내용과 UI와 상호 작용하는 방식을 모방하는 테스트를 작성하세요. 이는 Capybara의 의미적 메서드를 선호하고 ID, 클래스 또는 속성으로 쿼리하지 않도록 하는 것을 의미합니다.
이 방식으로 테스트하는 장점은 다음과 같습니다.
- 모든 상호작용 요소에 접근 가능한 이름이 있도록 보장합니다.
- 언어가 더 자연스럽기 때문에 가독성이 높습니다.
- 사용자에게 보이지 않는 ID, 클래스 및 속성으로 쿼리하는 것을 피하므로 연약하지 않습니다.
필요한 경우, within
을 사용하여 페이지의 특정 영역 내에서 상호작용을 범위화할 수 있습니다.
보통 라벨이없는 div
와 같은 요소의 범위를 제한할 것이므로, 이 경우에는 datatestid
선택기를 사용할 수 있습니다.
axe 자동 접근성 테스트를 UI 테스트에서 실행하려면 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_by_testid('element')
Matchers
가능한 경우 아래와 같은 더 구체적인 matchers를 사용하세요.
# 좋은 예
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 modals과 상호작용하려면 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
기타 유용한 메서드
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('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]}}
Current example is paused for live debugging
The current user credentials are: user2 / 12345678
Press any key to resume the execution of the example!
Back to the example!
.
Finished in 34.51 seconds (files took 0.76702 seconds to load)
1 example, 0 failures
live_debug
은 JavaScript가 활성화된 특정 조건에서만 작동합니다.
가시적 브라우저에서 :js
특성 실행
다음과 같이 WEBDRIVER_HEADLESS=0
로 스펙을 실행하세요.
WEBDRIVER_HEADLESS=0 bin/rspec some_spec.rb
테스트가 빠르게 완료되지만 무슨 일이 일어나고 있는지에 대한 감을 제공합니다.
WEBDRIVER_HEADLESS=0
을 사용하여 live_debug
를 사용하면 열려 있는 브라우저를 일시 중지시키고 페이지를 다시 열지 않습니다.
이는 디버깅 및 요소 검사에 사용할 수 있습니다.
또한 실행을 일시 중지하고 테스트를 진행하기 위해 byebug
또는 binding.pry
를 추가할 수 있습니다.
스크린샷
실패 시 자동으로 스크린샷을 찍는 capybara-screenshot
젬을 사용합니다. CI에서 이러한 파일을 작업 artifacts로 다운로드할 수 있습니다.
또한 다음 메서드를 추가하여 테스트 중에 수동으로 스크린샷을 찍을 수 있습니다. 더 이상 필요하지 않은 경우 삭제해야 합니다! 자세한 내용은 https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots에서 확인할 수 있습니다.
:js
스펙에서 screenshot_and_save_page
를 추가하여 Capybara가 “보는” 것을 스크린샷을 찍고 페이지 소스를 저장하세요.
:js
스펙에서 screenshot_and_open_image
를 추가하여 Capybara가 “보는” 것을 스크린샷을 찍고 이미지를 자동으로 열어주세요.
이로 인해 생성된 HTML 덤프에는 CSS가 없습니다. 이는 실제 애플리케이션과 매우 다르게 보입니다. 디버깅을 쉽게 하기 위해 작은 해킹으로 CSS를 추가할 수 있습니다.
빠른 단위 테스트
일부 클래스는 Rails에서 잘 격리되어 있습니다. 이러한 경우에는 테스트 파일에서 require 'spec_helper'
대신에 require 'fast_spec_helper'
을 사용하여 테스트를 실행하면 매우 빠르게 실행될 수 있어야 합니다. 그 이유는 다음과 같습니다.
- 젬 로딩이 건너뛰어집니다.
- Rails 앱 부트가 건너뛰어집니다.
- GitLab Shell 및 Gitaly 설정이 건너뛰어집니다.
- 테스트 저장소 설정이 건너뛰어집니다.
일반적인 spec_helper
의 경우 30초 이상이 걸리는 테스트의 경우, fast_spec_helper
를 사용하는 테스트는 1초 정도 소요됩니다.
fast_spec_helper
는 또한 lib/
디렉토리에 위치한 클래스를 자동으로 로딩하는 기능도 지원합니다. 만약 클래스나 모듈이 lib/
디렉토리에서만 코드를 사용하는 경우에는 명시적으로 어떤 의존성을 로드할 필요가 없습니다. fast_spec_helper
는 또한 Rails 환경에서 일반적으로 사용되는 핵심 확장 프로그램을 포함한 모든 ActiveSupport 확장 프로그램을 로드합니다.
다만, 때에 따라서 일부 코드가 젬을 사용하거나 의존성이 lib/
에 있지 않은 경우에는 require_dependency
를 사용하여 몇 가지 의존성을 로드해야 할 수 있습니다.
예를 들어, Gitlab::UntrustedRegexp
클래스를 사용하는 코드를 테스트하려면, 내부적으로 re2
라이브러리를 사용하게 됩니다. 여기서 고려해야 할 사항은 다음과 같습니다.
-
re2
젬을 필요로 하는 사용자 라이브러리 파일에require_dependency 're2'
를 추가하여 이 요구 사항을 명시적으로 만드는 것이 좋습니다. - 테스트 스펙 자체에 추가하십시오.
또는, 해당 의존성이 여러 다른 fast_spec_helper
스펙에서 필요한 경우에는 의존성을 여러 번 수동으로 추가하고 싶지 않은 경우에는 fast_spec_helper
에서 직접 호출하도록 추가할 수 있습니다. 이를 위해 spec/support/fast_spec/YOUR_DOMAIN/fast_spec_helper_support.rb
파일을 만들어 fast_spec_helper
에서 요구하십시오. 이에 관한 기존 예제를 따를 수 있습니다.
RuboCop 관련 스펙에는 rubocop_spec_helper
을 사용하십시오.
경고:
코드와 해당 스펙이 Rails에서 잘 격리되어 있는지 확인하려면 bin/rspec
을 통해 스펙을 개별적으로 실행하십시오. bin/spring rspec
을 사용하지 마십시오. 왜냐하면 bin/spring rspec
은 자동으로 spec_helper
을 로드하기 때문입니다.
subject
및 let
변수
GitLab RSpec 스위트는 중복을 줄이기 위해 let
과(let
의 엄격한, 지연되지 않는 버전인 let!
) 변수를 적극적으로 활용했습니다. 그러나 이는 때로 명확성의 비용을 초래할 수 있기 때문에 앞으로 사용에 대한 일부 지침을 설정해야합니다.
-
let!
변수가 인스턴스 변수보다 선호됩니다.let
변수가let!
변수보다 선호됩니다. 로컬 변수가let
변수보다 선호됩니다. - 전체 스펙 파일 전체에 걸쳐 중복을 줄이기 위해
let
을 사용하십시오. - 단일 테스트에서 사용되는 변수를 정의하는 데
let
을 사용하지 마십시오. 해당 변수들은 테스트의it
블록 내의 로컬 변수로 정의하십시오. - 상위 수준
describe
블록 내에서 사용되지 않는let
변수를 정의하지 마십시오. 쓰이는 곳에 가까운 곳에 정의하십시오. - 하나의
let
변수의 정의를 다른 것으로 오버라이드하는 것을 피하십시오. - 다른 변수의 정의에 사용되는
let
변수를 정의하지 마십시오. 대신 도우미 메소드를 사용하십시오. - 엄격한 평가와 정의된 순서를 필요로 할 때만
let!
변수를 사용하십시오. 그 외에,let
만으로 충분합니다.let
은 지연되기 때문에 해당 변수를 참조할 때까지는 계산되지 않습니다. - 예제에서
subject
를 참조하는 것을 피하십시오. 명명된subject(:name)
나let
변수를 대신 사용하여 변수에 컨텍스트 이름을 지정하십시오. - 예제 안에서
subject
가 참조되지 않는 경우, 이름이 지정되지 않은subject
를 정의하는 것은 허용됩니다.
공통 테스트 설정
주의:
let_it_be
와 before_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
, User
, ProjectMember
만이 생성됩니다.
let_it_be
와 before_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_be
는 before(:all)
블록에서 발생하기 때문에 RSpec에서 before(:all)
에서 절대적으로 스탭을 허용하지 않기 때문에 allow
과 같은 스텁을 사용하는 팩토리와 함께 사용할 수 없습니다. 자세한 내용은 이슈를 참조하십시오. 이를 해결하려면 let
을 사용하거나 해당 팩토리를 스텁을 사용하지 않도록 변경하십시오.
시간에 민감한 테스트
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 도우미
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)
훅과 블록 구문을 사용하여 구현됩니다.
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
를 사용하여 설정됩니다.
시간 정밀도는 OS에 따라 다릅니다.
문서에 명시된 대로 소수 초를 포함할 수 있습니다.
Rails 모델이 데이터베이스에 저장될 때, 가지고 있는 모든 타임스탬프들은 PostgreSQL에서 timestamp without time zone
이라는 유형을 사용하여 저장됩니다. 이 유형은 마이크로초 해상도를 가지며, 즉 소수점 이하에 여섯 자리가 있습니다. 따라서 1577987974.6472975
가 PostgreSQL에 전송되면, 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 {"customer_relations_contacts"=>[{:created_at=>"2023-08-04T13:30:20Z", :first_name=>"Sidney Jones3"}]} to include {"customer_relations_contacts"=>[{:created_at=>"2023-08-04 13:30:20.245964000 +0000", :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의 블로그 글에서 가져왔습니다.
이 문제가 발생한 MR(병합 요청)와 이에 대해 토론된 백엔드 페어링 세션을 확인할 수 있습니다.
테스트에서 기능 플래그
이 섹션은 기능 플래그로 개발하기로 이동되었습니다.
원점 테스트 환경
단일 GitLab 테스트에서 실행된 코드는 많은 데이터 항목에 액세스하고 수정할 수 있습니다. 테스트가 실행되기 전에 신중한 준비와 이후의 정리 없이 데이터를 변경할 경우, 후속 테스트의 동작에 영향을 미칠 수 있습니다. 가능한한 이러한 상황을 피해야 합니다! 다행스럽게도, 기존의 테스트 프레임워크는 이미 대부분의 경우를 처리합니다.
테스트 환경이 오염되었을 때 공통 결과는 flaky tests입니다. 오염은 종종 순서 의존성으로 나타납니다: spec A를 실행한 후 spec B를 실행하면 항상 실패하지만, spec B를 실행한 후 spec A를 실행하면 항상 성공합니다. 이 경우 rspec --bisect
를 사용하여 (또는 spec 파일을 직접 이진 탐색하여) 누락된 spec을 결정할 수 있습니다. 문제를 해결하려면 테스트 스위트가 환경이 어떻게 원점 상태로 보장하는지에 대한 이해가 필요합니다. 각 데이터 저장소에 대해 자세히 알아보세요!
SQL 데이터베이스
이것은 database_cleaner
젬에 의해 우리를 위해 관리됩니다. 각 spec은 트랜잭션 안에 둘러싸이며, 테스트가 완료된 후에 롤백됩니다. 특정한 spec은 대신 완료 후에 각 테이블에 대해 DELETE FROM
쿼리를 발행합니다. 이것은 생성된 행이 여러 데이터베이스 연결에서 볼 수 있게 하므로, 브라우저에서 실행되는 특수 사양이나 마이그레이션 사양과 같은 것들에 중요합니다.
이러한 전략을 사용하는 것의 결과로 TRUNCATE TABLES
방식이 아닌 경우 기본 키와 다른 시퀀스가 재설정되지 않는다는 것입니다. 따라서 spec A에서 프로젝트를 생성한 다음 spec B에서 프로젝트를 생성하면, 첫 번째는 id=1
을 갖고 두 번째는 id=2
를 갖게 됩니다.
이 말은 specs은 ID 값이나 다른 시퀀스로 생성된 열의 값을 의존해서는 안 되며, 실수로 충돌을 피하기 위해 이런 종류의 열에 대한 값을 수동으로 지정하는 것도 피해야 한다는 의미입니다. 대신, 그들을 명시하지 않고 행을 생성한 후에 그 값을 조회해야 합니다.
이주의 TestProf
위에서 설명한 것 때문에 마이그레이션 사양에서는 데이터베이스 트랜잭션 내에서 실행될 수 없습니다. 저희의 테스트 수트는 실행 시간을 개선하기 위해 TestProf를 사용하지만, TestProf
는 이러한 최적화를 수행하기 위해 데이터베이스 트랜잭션을 사용합니다. 이러한 이유로 마이그레이션 사양에서 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 작업 두 가지 주요 데이터 범주를 저장합니다. 여기에서 Gitlab::Redis::Wrapper
하위 항목 목록을 확인할 수 있습니다. 별도의 Redis 인스턴스에서 백업됩니다.
대부분의 사양에서 Rails 캐시는 실제로 메모리 내 저장소입니다. 이는 사양 간에 교체되므로, Rails.cache.read
및 Rails.cache.write
호출은 안전합니다. 그러나 특정 사양에서 직접적인 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 작업 처리에 의존하지 않도록 수정되어야 하거나, 백그라운드 작업 처리가 필요한/예상된 경우 :sidekiq_might_not_need_inline
특성이 :sidekiq_inline
로 업데이트되어야 합니다.
perform_enqueued_jobs
의 사용은 지연된 메일 전송을 테스트할 때에만 유용합니다. 왜냐하면 저희의 Sidekiq worker들은 ApplicationJob
/ActiveJob::Base
를 상속받지 않기 때문입니다.
DNS
로컬 네트워크에 따라 DNS가 문제를 일으킬 수 있기 때문에 !22368에서 테스트 수트 전반에 걸쳐 DNS 요청이 스텁화됩니다. spec/support/dns.rb
에서 사용할 수 있는 RSpec 레이블이 있으므로, DNS 스텁화를 우회해야 하는 테스트에 적용할 수 있습니다.
it "실제로 Prometheus에 연결합니다", :permit_dns do
더 구체적인 제어가 필요한 경우, DNS 차단은 spec/support/helpers/dns_helpers.rb
에서 구현되었으며, 이 메서드들을 다른 위치에서 호출할 수 있습니다.
요율 제한
요율 제한은 테스트 수트에서 활성화되어 있습니다. :js
특성을 사용하는 피처 사양에서 요율 제한이 트리거될 수 있습니다. 대부분의 경우, :clean_gitlab_redis_rate_limiting
특성을 사양에 표시함으로써 요율 제한 트리거를 피할 수 있습니다. 이 특성은 사양 간에 저장된 요율 제한 데이터를 지웁니다. 단일 테스트가 요율 제한을 트리거하면, :disable_rate_limit
대신 사용할 수 있습니다.
파일 메서드 스텁화
파일의 내용을 스텁화해야 하는 상황에서는 stub_file_read
및 expect_file_read
도우미 메서드를 사용하세요. 이 메서드들은 File.read
를 올바르게 스텁화하며, 주어진 파일 이름에 대해 File.read
를 스텁하고 File.exist?
도 true
를 반환하도록 합니다.
어떤 이유에서든지 File.read
를 수동으로 스텁화해야 하는 경우에는 다음을 확인하세요:
- 다른 파일 경로에서 스텁하고 원래 구현을 호출하세요.
- 그런 다음
File.read
를 관심 있는 파일 경로에 대해서만 스텁하세요.
그렇지 않으면 코드베이스의 다른 부분에서 File.read
호출이 잘못 스텁화될 수 있습니다.
# 나쁜 예, 모든 파일이 읽혀지고 아무것도 반환되지 않습니다
allow(File).to receive(:read)
# 좋은 예
stub_file_read(my_filepath, content: "가짜 파일 내용")
# 좋은 예
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(my_filepath).and_return("가짜 파일_내용")
파일 시스템
파일 시스템 데이터는 “저장소”와 “나머지 모든 것”으로 대략 나눌 수 있습니다. 저장소는 tmp/tests/repositories
에 저장됩니다. 이 디렉터리는 테스트 실행 전에 비워지고, 테스트 실행이 끝난 후에도 비워집니다. 그러나 사양 간에는 비워지지 않으므로, 생성된 저장소는 이 디렉터리에 점점 누적됩니다. 그것들을 삭제하는 데는 시간이 걸리지만, 관리를 조심히 하지 않으면 오염될 수 있습니다.
이를 피하기 위해 해시 저장소가 테스트 수트에서 활성화됩니다. 이것은 저장소가 그들의 프로젝트 ID에 따라 결정되는 고유한 경로를 갖기 때문입니다. 프로젝트 ID가 사양 간에 재설정되지 않기 때문에, 각 사양은 자신의 디스크에 고유한 저장소를 갖게 되어, 사양 간에 변경 내용이 보이는 것을 막습니다.
사양이 프로젝트 ID를 수동으로 지정하거나 tmp/tests/repositories/
디렉터리의 상태를 직접적으로 검사하는 경우, 그 사양이 실행 전후에 이 디렉터리를 정리해야 합니다. 일반적으로, 이러한 패턴은 완전히 피하는 것이 좋습니다.
해당 디렉터리에 디스크상에서 저장된 다른 파일 클래스들, 예를 들어 업로드와 같은 데이터베이스 객체와 연결된 파일들은 보통 동일한 방식으로 관리됩니다. 테스트 수트에서 해시 저장소가 활성화되면 ID에 따라 디스크에 기록되므로 충돌이 발생하지 않습니다.
일부 사양은 projects
팩토리에 :legacy_storage
특성을 전달함으로써 해시 저장소를 비활성화합니다. 이를 하는 사양은 절대로 프로젝트의 path
나 해당 그룹 중 하나를 재정의해서는 안 됩니다. 기본 경로는 프로젝트 ID를 포함하므로 충돌되지 않습니다. :legacy_storage
프로젝트로 동일한 경로에 프로젝트를 생성하는 두 사양은 디스크상에서 동일한 저장소를 사용하게 되어, 테스트 환경 오염을 일으킬 수 있습니다.
그 외의 파일들은 사양에 의해 수동으로 관리되어야 합니다. 예를 들어, tmp/test-file.csv
파일을 생성하는 코드를 실행하는 경우, 해당 파일이 정리되어야 합니다.
영구적인 인-메모리 애플리케이션 상태
주어진 rspec
실행에서 모든 사양은 동일한 Ruby 프로세스를 공유하므로
사양 간에 접근 가능한 Ruby 객체를 수정하여 서로 영향을 미칠 수 있습니다. 실제로는 전역 변수와 상수 (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
특성은 파이프라인의 실행 시간을 줄이기 위해 시작 및 종료 시 context 간에 색인을 생성하고 삭제합니다. Elasticsearch delete by query API
를 사용하여 예제 간에 색인된 데이터를 삭제하여 깨끗한 색인을 보장합니다.
elastic_clean
특성은 예제 간에 색인을 생성하고 삭제하여 깨끗한 인덱스를 보장합니다. 이렇게 하면 테스트가 비필수적인 데이터로 오염되지 않습니다. :elastic
또는 :elastic_delete_by_query
특성을 사용하면 문제가 발생할 경우 대신 :elastic_clean
을 사용하십시오. :elastic_clean
은 다른 특성보다 훨씬 느리며 절약해서 사용해야 합니다.
Elasticsearch 논리의 대부분의 테스트는 다음과 관련이 있습니다:
- PostgreSQL에 데이터 생성 및 Elasticsearch에 색인이될 때까지 대기.
- 해당 데이터 검색.
- 테스트가 예상한 결과를 제공하는지 확인.
개별 레코드 대신 인덱스 구조적 변경을 확인하는 등 몇 가지 예외 사항이 있습니다.
참고:
Elasticsearch 색인에는 Gitlab::Redis::SharedState
가 사용됩니다.
따라서 Elasticsearch 특성은 동적으로 :clean_gitlab_redis_shared_state
특성을 사용합니다.
수동으로 :clean_gitlab_redis_shared_state
를 추가할 필요가 없습니다.
Elasticsearch를 사용하는 사양은 다음을 필요로 합니다:
- PostgreSQL에 데이터 생성한 다음 Elasticsearch에 색인 추가.
- Elasticsearch용 Application 설정 활성화 (기본적으로 비활성화).
아래의 방법을 사용하여:
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
또한 Elasticsearch의 비동기적 성질을 극복하기 위해 ensure_elasticsearch_index!
메서드를 사용할 수 있습니다. 이는 Elasticsearch Refresh API
를 사용하여 마지막 리프레시 이후 인덱스에 수행된 모든 작업을 검색할 수 있도록 보장합니다. 이 메서드는 일반적으로 데이터를 PostgreSQL에 로드 한 후에 데이터가 색인화되고 검색 가능한지 확인하기 위해 호출됩니다.
SEARCH_SPEC_BENCHMARK
환경 변수를 사용하여 테스트 설정 단계의 벤치마크 테스트를 할 수 있습니다:
SEARCH_SPEC_BENCHMARK=1 bundle exec rspec ee/spec/lib/elastic/latest/merge_request_class_proxy_spec.rb
Snowplow 이벤트 테스트
경고:
Snowplow는 contracts gem을 사용하여 런타임 유형 검사를 수행합니다.
Snowplow는 기본적으로 테스트 및 개발 중에 비활성화되므로 Gitlab::Tracking
을 모킹할 때 예외를 잡는 것이 어려울 수 있습니다.
유형 검사로 인한 런타임 오류를 잡기 위해 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
category
및 action
을 생략할 수 있지만 플래키 테스트를 피하려면 적어도 category
를 지정해야 합니다. 예를 들어,
Users::ActivityService
는 API 요청 후에 Snowplow 이벤트를 추적하며, expect_no_snowplow_event
는 요청 없이 실행할 경우 실패합니다.
스키마에 대한 Snowplow 컨텍스트 테스트
Snowplow 스키마 매처는 Snowplow 컨텍스트를 JSON 스키마에 대해 테스트하여 유효성 오류를 줄이는 데 도움이 됩니다. 스키마 매처는 다음과 같은 매개변수를 허용합니다:
스키마 경로
컨텍스트
스키마 매처 사양을 추가하려면:
-
Iglu 리포지토리에 새 스키마를 추가한 다음 동일한 스키마를
spec/fixtures/product_intelligence/
디렉토리에 복사하십시오. - 복사된 스키마에서
"$schema"
키와 값을 제거하십시오. 사양 안에서 사용하지 않기 때문에 유지하면 스펙이 실패합니다. -
다음 스니펫을 사용하여 스키마 매처를 호출하십시오:
match_snowplow_context_schema(schema_path: '<스텝 1에서의 파일 이름>', context: <컨텍스트 해시>)
테이블 기반 / 매개변수화된 테스트
이 스타일의 테스트는 특정 범위의 입력 값으로 코드 한 조각을 연습하는 데 사용됩니다. 한 번의 테스트 케이스를 지정하여 각각의 기대되는 출력과 함께 입력 값 테이블과 함께 사용함으로써, 테스트가 보다 읽기 쉽고 더 조밀하게 작성될 수 있습니다.
RSpect::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 variables must be referenced using `ref`
ref(:one) | ref(:two) | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
테이블 기반 테스트를 작성한 뒤에 다음과 같은 오류가 표시된다면:
NoMethodError:
undefined method `to_params'
param_sets = extracted.is_a?(Array) ? extracted : extracted.to_params
^^^^^^^^^^
Did you mean? to_param
그것은 테스트 파일에 using RSpec::Parameterized::TableSyntax
의 줄을 포함해야 한다는 것을 나타냅니다.
경고:
where
블록에서 입력 값으로 간단한 값을 사용하세요. 프록, 상태를 갖는 객체, FactoryBot로 생성된 객체 및 유사한 항목을 사용하는 것은 예기치 못한 결과를 낳을 수 있습니다.
Prometheus 테스트
Prometheus 메트릭은 한 번의 테스트 실행에서 다음으로 유지될 수 있습니다. 메트릭이 각 예제 전에 재설정되도록 하려면 RSpec 테스트에 :prometheus
태그를 추가하세요.
Matchers
사용자 정의 Matchers는 RSpec 기대의 목적을 명확하게 하거나/및 복잡성을 숨기기 위해 작성되어야 합니다. 이들은 spec/support/matchers/
아래에 배치되어야 합니다. Matchers는 특정 유형의 특정 유형의 specs(예: features 또는 requests)에만 해당된다면 하위 폴더에 배치되어야 하지만, 여러 유형의 specs에 대해 해당되는 경우에는 하위 폴더에 배치되어서는 안 됩니다.
be_like_time
데이터베이스에서 반환된 시간은 루비의 시간 객체의 정밀도와 다를 수 있으므로, 스펙에서 비교할 때 유연한 허용값이 필요합니다.
PostgreSQL의 시간 및 타임스탬프 유형은 1마이크로초의 해상도를 갖습니다. 그러나 루비 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
의 정밀도가 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_status
를 have_http_status
및 expect(response.status).to
대신 사용하세요. 왜냐하면 전자는 상태 불일치시 응답 body를 표시할 수 있기 때문에 매우 유용합니다. 어떤 테스트가 실패하고 왜 실패했는지 알고 싶을 때 매우 유용합니다.
특히 500 내부 서버 오류를 표시할 때 매우 유용합니다.
숫자 표현 대신 :no_content
와 같은 명명된 HTTP 상태를 선호하세요. 지원되는 상태 코드 목록을 참조하세요.
예제:
expect(response).to have_gitlab_http_status(:ok)
match_schema
및 match_response_schema
match_schema
매처는 주제가 JSON 스키마와 일치하는지를 유효화할 수 있습니다. expect
내부 항목은 JSON 문자열 또는 JSON 호환 데이터 구조가 될 수 있습니다.
match_response_schema
는 요청 스펙에서 응답 객체를 사용하기 위한 편의 매처입니다.
예제:
# spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json에 일치함
expect(data).to match_schema('prometheus/additional_metrics_query_result')
# ee/spec/fixtures/api/schemas/board.json에 일치함
expect(data).to match_schema('board', dir: 'ee')
# Ruby 데이터 구조로 이루어진 스키마에 대해 일치함
expect(data).to match_schema(Atlassian::Schemata.build_info)
be_valid_json
be_valid_json
은 문자열이 JSON으로 구문 분석되고 비어 있지 않은 결과를 반환하는지를 유효화할 수 있습니다. 상기 스키마 일치와 결합하려면 and
를 사용하세요:
expect(json_string).to be_valid_json
expect(json_string).to be_valid_json.and match_schema(schema)
be_one_of(collection)
include
의 반대로, collection
이 예상 값 포함을 테스트합니다:
expect(:a).to be_one_of(%i[a b c])
expect(:z).not_to be_one_of(%i[a b c])
쿼리 성능 테스트
쿼리 성능을 테스트하는 것은 다음을 가능케 합니다:
- 코드 블록에서 N+1 문제가 존재하지 않음을 확인합니다.
- 코드 블록 내의 쿼리 수가 눈치채지 못한 채로 증가하지 않도록 보장합니다.
QueryRecorder
QueryRecorder
는 주어진 코드 블록에서 수행된 데이터베이스 쿼리 수를 프로파일링하고 테스트할 수 있게 합니다.
자세한 내용은 QueryRecorder
섹션을 참조하세요.
GitalyClient
Gitlab::GitalyClient.get_request_count
를 사용하여 주어진 코드 블록에서 수행된 Gitaly 쿼리 수를 테스트할 수 있습니다.
자세한 내용은 Gitaly Request Counts
섹션을 참조하세요.
공유된 컨텍스트
한 가지 spec 파일에서만 사용되는 공유된 컨텍스트는 인라인으로 선언될 수 있습니다. 하나 이상의 spec 파일에서 사용되는 공유된 컨텍스트는 다음과 같이 선언해야 합니다:
-
spec/support/shared_contexts/
아래에 배치되어야 합니다. - 일부 타입의 spec에만 해당되는 경우 하위폴더에 배치될 수 있지만, 여러 타입의 spec에 해당되는 경우 하위폴더에 배치되어서는 안 됩니다.
각 파일은 하나의 컨텍스트만을 포함하고, spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb
와 같이 기술적인 이름을 가져야 합니다.
공유된 예시
한 가지 spec 파일에서만 사용되는 공유된 예시는 인라인으로 선언될 수 있습니다. 하나 이상의 spec 파일에서 사용되는 공유된 예시는 다음과 같이 선언해야 합니다:
-
spec/support/shared_examples/
아래에 배치되어야 합니다. - 일부 타입의 spec에만 해당되는 경우 하위폴더에 배치될 수 있지만, 여러 타입의 spec에 해당되는 경우 하위폴더에 배치되어서는 안 됩니다.
각 파일은 하나의 컨텍스트만을 포함하고, spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb
와 같이 기술적인 이름을 가져야 합니다.
헬퍼(도우미)
헬퍼는 보통 특정 RSpec 예시의 복잡성을 감추기 위해 일부 메서드를 제공하는 모듈입니다. 헬퍼를 RSpec 파일에 정의하는 경우 다른 예시와 공유되지 않을 경우에만 정의해야 합니다. 그렇지 않으면 spec/support/helpers/
아래에 배치되어야 합니다. 헬퍼는 특정 타입의 spec에만 해당되는 경우에는 하위폴더에 배치될 수 있지만, 여러 타입의 spec에 해당되는 경우에는 하위폴더에 배치되어서는 안 됩니다.
헬퍼는 Rails 네이밍 / 네임스페이싱 규칙을 따라야 합니다. 예를 들어, spec/support/helpers/features/iteration_helpers.rb
는 다음을 정의해야 합니다:
# frozen_string_literal: true
module Features
module IterationHelpers
def iteration_period(iteration)
"#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
end
end
end
헬퍼는 RSpec 구성을 변경해서는 안 됩니다. 예를 들어 위에서 설명한 헬퍼 모듈은 다음과 같이 포함해서는 안 됩니다:
# 나쁨
RSpec.configure do |config|
config.include Features::IterationHelpers
end
# 좋음, 특정 spec에 포함
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include Features::IterationHelpers
end
Ruby 상수 테스트
Ruby 상수를 사용하는 코드를 테스트할 때는 상수의 값보다는 해당 상수에 의존하는 동작에 초점을 맞추어야 합니다.
예를 들어, 다음은 클래스 메서드 .categories
의 동작을 테스트하기 때문에 선호됩니다.
describe '.categories' do
it 'gets CE unique category names' do
expect(described_class.categories).to include(
'deploy_token_packages',
'user_packages',
# ...
'kubernetes_agent'
)
end
end
반면에 상수 자체의 값을 테스트하는 것은 코드와 테스트에서 값들을 반복하기 때문에 적은 가치를 제공합니다.
describe CATEGORIES do
it 'has values' do
expect(CATEGORIES).to eq([
'deploy_token_packages',
'user_packages',
# ...
'kubernetes_agent'
])
end
end
상수에 대한 오류가 치명적인 영향을 미칠 수 있는 중요한 경우에는, 추가적인 보호장치로서 상수 값을 테스트하는 것이 유용할 수 있습니다. 예를 들어, 전체 GitLab 서비스를 중단시킬 수 있거나 고객에게 예상보다 더 많은 청구가 될 수 있거나 우주가 붕괴될 수 있는 경우에 해당합니다.
팩토리
GitLab은 테스트 픽스처 대체로 factory_bot
을 사용합니다.
- 팩토리 정의는
spec/factories/
에 위치하며, 해당 모델의 복수형으로 명명됩니다(User
팩토리는users.rb
에 정의됩니다). - 파일 당 최상위 팩토리 정의는 하나만 있어야 합니다.
- FactoryBot 메서드는 모든 RSpec 그룹에 혼합되어 있습니다. 따라서
FactoryBot.create(...)
대신에create(...)
를 호출해야 합니다. - 정의할 때, 결과 레코드가 유효성 검사를 통과하기 위해 필요하지 않은 속성을 정의하면 안 됩니다.
- 팩토리에서 인스턴스화할 때, 테스트에 필요하지 않은 속성을 제공해서는 안 됩니다.
-
콜백에서 연관 설정을 위해
create
/build
대신 암시적, 명시적, 또는 인라인 연관을 사용해야 합니다. 추가적인 맥락은 이슈 #262624에서 확인할 수 있습니다.has_many
와belongs_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
- 팩토리는
ActiveRecord
객체에만 국한되지 않아도 됩니다. 예시 참조. - 팩토리와 해당 트레이트는 모든 모델 spec에서 실행되는 공유된 스펙에 의해 확인된 유효한 객체를 생성해야 합니다.
- 팩토리에서
skip_callback
의 사용을 피해야 합니다. 자세한 내용은 이슈 #247865를 참조하세요.
픽스처
모든 픽스처는 spec/fixtures/
아래에 위치해아 합니다.
저장소
병합 요청을 병합하는 기능과 같은 일부 기능을 테스트하려면 특정 상태의 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
호출에 수정자(modifiers)를 추가해야 합니다. 예를 들어, spec/support/helpers/cycle_analytics_helpers.rb
가 :lib
및 type: :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
파일에서 사용됩니다. 자세한 내용은 빠른 단위 테스트를 참조하십시오.
테스트 환경 로깅
테스트 환경용 서비스는 테스트가 실행될 때 자동으로 구성되고 시작됩니다. 이에는 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