- 테스트 설계
-
RSpec
- 일반 가이드라인
- 애플리케이션 코드의 지연 로딩
- 루비 경고
- 테스트 순서
- 테스트 불안정성
- 테스트 속도 저하
- 기능 카테고리 메타데이터
- EE 라이센스에 의존하는 테스트
- SaaS에 의존하는 테스트
- 커버리지
- 시스템 / 기능 테스트
- 빠른 단위 테스트
subject
및let
변수- 일반 테스트 설정
- 시간 민감 테스트
- 테스트에서의 기능 플래그
- 순수 테스트 환경
- 테이블 기반 / 매개변수화된 테스트
- 프로메테우스 테스트
- 매처
- 쿼리 성능 테스트
- 공유 컨텍스트
- 공유 예제
- 헬퍼
- 루비 상수 테스트
- 팩토리
- Fixtures
- Repositories
- Configuration
- Test environment logging
테스트 모범 사례
테스트 설계
GitLab에서 테스트는 1급 시민이며, 후순위가 아닙니다. 기능 설계와 마찬가지로 테스트 설계도 고려하는 것이 중요합니다.
기능을 구현할 때, 올바른 방식으로 올바른 기능을 개발하는 것에 대해 생각합니다. 이는 우리의 범위를 관리 가능한 수준으로 좁히는 데 도움이 됩니다. 기능에 대한 테스트를 구현할 때는 올바른 테스트를 개발하는 것뿐만 아니라 테스트가 실패할 수 있는 모든 중요한 방식을 고려해야 합니다. 이는 빠르게 관리하기 어려운 수준으로 범위를 넓힐 수 있습니다.
테스트 휴리스틱은 이 문제를 해결하는 데 도움이 될 수 있습니다. 이는 코드에서 버그가 나타나는 일반적인 방법을 간결하게 다룹니다. 테스트 설계 시, 알려진 테스트 휴리스틱을 검토하여 우리의 테스트 설계를 알리는 데 시간을 할애하세요. 우리는 핸드북의 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=1 bundle exec guard
를 대신 사용하여 spring을 활용하세요.
일반 가이드라인
- 단일 최상위
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 매처(예:find('.js-foo')
)를 사용하세요. - 실행하고 싶고 격리된 테스트 부분에는
focus: true
를 사용하세요. - 테스트에 기대치가 하나 이상 있는 경우
:aggregate_failures
를 사용하세요. -
빈 테스트 설명 블록의 경우, 테스트가 스스로 설명적이라면
it do
보다는specify
를 사용하세요. - 실제로 존재하지 않는 ID/IID/access level이 필요할 때는
non_existing_record_id
/non_existing_record_iid
/non_existing_record_access_level
을 사용하세요. 123, 1234 또는 심지어 999를 사용하는 것은 불안정하며 CI 실행 문맥에서 이러한 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=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
에서 자동으로 제거합니다.
스펙이 검사를 통과하지 못하면 무작위 순서로 실행되기 전에 수정해야 합니다.
테스트 불안정성
테스트 불안정성을 피하기 위한 프로세스에 대한 자세한 내용은 Unhealthy tests 페이지를 참조하세요.
테스트 속도 저하
GitLab은 방대한 테스트 스위트를 보유하고 있으며, 병렬화 없이 실행하는 경우 몇 시간이 걸릴 수 있습니다.
정확하고 효과적이면서도 빠른 테스트를 작성하기 위해 노력하는 것이 중요합니다.
테스트 성능은 품질과 속도를 유지하는 데 중요하며,
CI 빌드 시간 및 고정 비용에 직접적인 영향을 미칩니다.
철저하고 정확하며 빠른 테스트가 필요합니다. 여기서는 이를 달성하기 위한 도구와 기술에 대한 정보를 찾을 수 있습니다.
테스트 속도 저하를 피하기 위한 프로세스에 대한 자세한 내용은 Unhealthy tests 페이지를 참조하세요.
필요하지 않은 기능 요청을 피하세요
우리는 예제나 상위 컨텍스트에 주석을 추가하여 기능을 추가하는 것을 쉽게 만듭니다.
이러한 예제는 다음과 같습니다:
-
:js
는 전체 JavaScript 가능 헤드리스 브라우저를 실행하는 기능 스펙입니다. -
:clean_gitlab_redis_cache
는 예제에 깨끗한 Redis 캐시를 제공합니다. -
:request_store
는 예제에 요청 저장소를 제공합니다.
우리는 테스트 종속성을 줄여야 하며, 기능을 피하는 것도 설정 필요성을 줄입니다.
:js
는 특히 피해야 합니다. 이는 기능 테스트가 브라우저에서 JavaScript 반응성을 요구하는 경우에만 사용해야 합니다 (예: Vue.js 구성 요소 클릭).
헤드리스 브라우저를 사용하는 것은 앱의 HTML 응답을 파싱하는 것보다 훨씬 느립니다.
프로파일링: 테스트가 시간을 소비하는 위치 확인하기
rspec-stackprof
는 테스트가 시간을 소비하는 위치를 보여주는 플레임 그래프를 생성하는 데 사용될 수 있습니다.
이 젬은 우리가 https://www.speedscope.app에 업로드할 수 있는 JSON 리포트를 생성합니다.
설치
stackprof
젬은 GitLab에 이미 설치되어 있습니다. 또한 JSON 리포트를 생성하는 스크립트(bin/rspec-stackprof
)도 제공합니다.
# 선택사항: JSON 리포트를 https://www.speedscope.app에 쉽게 업로드하려면 `speedscope` 패키지를 설치하세요.
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
보다 빠릅니다. -
create
대신build
,build_stubbed
,attributes_for
,spy
또는instance_double
을 사용하세요. 데이터베이스 지속성은 느립니다!
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
숫자 간의 뚜렷한 차이로 식별할 수 있습니다:
total top-level total time time per call top-level time name
208 0 9.5812s 0.0461s 0.0000s namespace
208 76 37.4214s 0.1799s 13.8749s project
위의 표는 우리가 명시적으로 namespace
객체를 생성하지 않음을 보여줍니다(top-level == 0
) - 모두 우리를 위해 암묵적으로 생성됩니다. 하지만 우리는 여전히 208개를 갖게 되고(각 프로젝트당 하나) 9.5초가 소요됩니다.
암묵적 부모 연관성의 이름이 지정된 팩토리 호출에 대해 단일 객체를 재사용하기 위해, FactoryDefault
를 사용할 수 있습니다:
RSpec.describe API::Search, factory_default: :keep do
let_it_be(:namespace) { create_default(:namespace) }
이제 우리가 생성하는 각 프로젝트는 이 namespace
를 사용하며, namespace: namespace
로 전달할 필요가 없습니다. 작동하도록 하려면 let_it_be
와 함께 factory_default: :keep
를 명시적으로 지정해야 합니다. 이는 테스트 스위트의 모든 예제에 대해 기본 팩토리를 유지하게 하여 각 예제에 대해 재생성하지 않도록 합니다.
우연한 의존성을 방지하기 위해, create_default
로 생성된 객체는
동결됩니다.
우리가 208개의 다른 프로젝트를 생성할 필요는 없을 것입니다 - 하나를 생성하고 재사용할 수 있습니다. 또한 우리가 생성하는 프로젝트 중 약 1/3만 요청한 프로젝트라는 것을 알 수 있습니다(76/208). 프로젝트에 대한 기본 값을 설정하는 것도 이점이 있습니다:
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
이 좋습니다. -
let_it_be_with_refind
는let_it_be_with_reload
와 비슷하게 작동하지만, 전자는ActiveRecord::Base#find
를 호출하고,ActiveRecord::Base#reload
는 호출하지 않습니다.reload
는 일반적으로refind
보다 빠릅니다. -
let_it_be_with_reload
는 같은 컨텍스트 내의 모든 예제에 대해 한 번만 객체를 생성하지만, 각 예제 후에 데이터베이스 변경 사항이 롤백되고,object.reload
가 호출되어 객체를 원래 상태로 복원합니다. 이로 인해 예제 이전이나 도중에 객체를 변경할 수 있습니다. 그러나 상태가 다른 모델로 누수되는 경우가 있을 수 있습니다. 이러한 경우, 예제가 몇 개 없을 때는let
이 더 쉬운 옵션이 될 수 있습니다. -
let_it_be
는 같은 컨텍스트 내의 모든 예제에 대해 한 번만 객체를 생성합니다. 이는let
과let!
의 훌륭한 대안이며, 각 예제에서 객체가 변경될 필요가 없을 때 유용합니다.let_it_be
를 사용하면 데이터베이스 모델을 생성하는 테스트 속도를 상당히 높일 수 있습니다. 자세한 정보와 예시는 https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be를 참조하세요.
프로 팁: 테스트를 작성할 때 let_it_be
내부의 객체를 불변(immutable)으로 간주하는 것이 가장 좋습니다. 이는 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
동결에 대한 더 많은 정보는 https://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection를 참조하세요.
let_it_be
는 객체를 한 번 인스턴스화하고 예제 간에 해당 인스턴스를 공유하므로 가장 최적화된 옵션입니다. 만약 let
대신 let_it_be
가 필요하다면, let_it_be_with_reload
를 시도해 보세요.
# 이전
let(:project) { create(:project) }
# 이후
let_it_be(:project) { create(:project) }
# 테스트에서 객체의 변경 사항을 기대해야 하는 경우
let_it_be_with_reload(:project) { create(:project) }
다음은 let_it_be
를 사용할 수 없지만 let_it_be_with_reload
가 let
보다 더 효율적일 수 있는 예입니다:
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
참고:
stub_method
는 let_it_be_with_refind
와 함께 사용할 때 작동하지 않습니다. 이는 stub_method
가 인스턴스에 메서드를 스텁하는 반면, let_it_be_with_refind
는 실행할 때마다 객체의 새 인스턴스를 생성하기 때문입니다.
stub_method
는 메서드 존재 여부 및 메서드 아리티 검사를 지원하지 않습니다.
경고:
stub_method
는 오로지 공장에서만 사용해야 합니다. 다른 곳에서 사용하는 것은 강력히 권장되지 않습니다. 사용 가능한 경우 RSpec mocks를 고려하세요.
멤버 접근 수준 스텁
Project
또는 Group
과 같은 팩토리 스텁에 대한 멤버 접근 수준을 스텁하려면
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
참고:
테스트 코드가 project_authorizations
또는 Member
기록을 유지하는 데 의존하는 경우 이 스텁 헬퍼를 사용하지 마세요. 대신 Project#add_member
또는 Group#add_member
를 사용하세요.
추가 프로파일링 메트릭
테스트를 실행할 때 만들어지는 SQL 쿼리 수를 진단하기 위해 rspec_profiling
젬을 사용할 수 있습니다.
이는 테스트와 관련이 없는 부분을 모킹하는 테스트로 인해 애플리케이션 측 SQL 쿼리가 발생하여 발생할 수 있습니다 (예: !123810).
느린 기능 테스트 문제 해결
느린 기능 테스트는 일반적으로 다른 테스트와 동일한 방법으로 최적화할 수 있습니다. 그러나 문제 해결 세션을 더 생산적으로 만드는 몇 가지 특정 기술이 있습니다.
UI에서 기능 테스트가 수행되는 것 확인
# 이전
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
# 이후
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
자세한 정보는 눈에 보이는 브라우저에서 :js
스펙 실행하기를 참고하세요.
프로파일링을 사용할 때 Capybara::DSL#
검색하기
stackprof
flamegraphs를 사용할 때, 검색에서 Capybara::DSL#
를 검색하여 수행된 capybara 작업과 해당 작업이 소요되는 시간을 확인하세요!
느린 테스트 식별하기
프로파일링을 사용하여 사양을 실행하는 것은 사양 최적화를 시작하는 좋은 방법입니다.
다음과 같이 할 수 있습니다:
bundle exec rspec --profile -- path/to/spec_file.rb
이것은 다음과 같은 정보를 포함합니다:
가장 느린 10개의 예제 (10.69초, 전체 시간의 7.7%):
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개의 보류
이 결과에서 우리는 사양에서 가장 비용이 많이 드는 예제를 확인할 수 있으며, 이것이 시작할 수 있는 장소입니다. 여기서 가장 비용이 많이 드는 예제는 공유 예제에 있으며, 일반적으로 여러 곳에서 호출되므로 감소가 더 큰 영향을 미칩니다.
비싼 작업 반복 피하기
격리된 예제는 매우 명확하며, 사양의 목적을 잘 수행하는 것을 돕지만, 다음 예제는 우리가 비싼 작업을 결합할 수 있는 방법을 보여줍니다:
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 'performs the expected side-effects' do
expect(BackgroundJob).to receive(:perform_async)
expect { subject.execute }
.to change(Event, :count).by(1)
.and change { arg_0.frobulance }.to('wibble')
end
이렇게 할 때는 조심해야 하며, 이는 성능 증가를 위해 명확성과 테스트 독립성을 희생하는 것입니다.
테스트를 결합할 때는 :aggregate_failures
를 사용하는 것을 고려하여 전체 결과를 사용할 수 있도록 하고, 첫 번째 실패만 보고되지 않도록 합니다.
막히신 경우
우리는 느린 백엔드 스펙을 리팩토링하는 데 도움을 줄 수 있는 사람들을 나열하는 backend_testing_performance
도메인 전문성을 가지고 있습니다.
도움이 필요한 사람들을 찾으려면 Engineering Projects 페이지에서 backend testing performance
를 검색하거나 the www-gitlab-org
project에서 직접 찾아보세요.
기능 카테고리 메타데이터
각 RSpec 예제에 대해 기능 카테고리 메타데이터를 설정해야 합니다.
EE 라이센스에 의존하는 테스트
FOSS_ONLY=1
로 실행하는지 여부에 따라 테스트를 실행하려면, context/spec 블록에서 if: Gitlab.ee?
또는 unless: Gitlab.ee?
를 사용할 수 있습니다.
예시: SchemaValidator는 라이선스에 따라 다른 경로를 읽습니다
SaaS에 의존하는 테스트
GitLab.com에서만 실행되는 코드를 테스트하려면 context/spec 블록에서 :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%를 커버하고 있는지 확인하세요.
시스템 / 기능 테스트
참고: 새로운 시스템 테스트를 작성하기 전에, 그 사용에 대한 이 가이드를 고려하세요
-
기능 스펙의 이름은
ROLE_ACTION_spec.rb
형식이어야 하며, 예를 들어user_changes_password_spec.rb
와 같습니다. -
성공 및 실패 경로를 설명하는 시나리오 제목을 사용하세요.
-
“성공적으로”와 같이 아무 정보도 추가하지 않는 시나리오 제목을 피하세요.
-
기능 제목을 반복하는 시나리오 제목을 피하세요.
-
데이터베이스에 필요한 레코드만 생성하세요.
-
행복한 경로와 덜 행복한 경로만 테스트하세요, 그게 전부입니다.
-
가능한 모든 다른 경로는 단위 또는 통합 테스트로 테스트해야 합니다.
-
페이지에 표시되는 것을 테스트하고, ActiveRecord 모델의 내부는 테스트하지 마세요. 예를 들어, 레코드가 생성되었는지 확인하려면, 레코드의 속성이 페이지에 표시되는지에 대한 기대를 추가하세요.
Model.count
가 한 개 증가했다고 기대하지 마세요. -
DOM 요소를 찾는 것은 괜찮지만, 그것을 남용하지 마세요. 왜냐하면 그러면 테스트가 더 취약해지기 때문입니다.
UI 테스트
UI를 테스트할 때, 사용자가 보는 것과 UI와 상호작용하는 방식을 시뮬레이션하는 테스트를 작성하세요.
이는 Capybara의 의미론적 메서드를 선호하고 ID, 클래스 또는 속성을 통해 쿼리하는 것을 피하는 것을 의미합니다.
이러한 방식으로 테스트하는 것의 장점은 다음과 같습니다:
-
모든 상호작용 요소가 접근 가능한 이름을 가지고 있는지 확인합니다.
-
더 자연어를 사용하므로 가독성이 높습니다.
-
사용자가 볼 수 없는 ID, 클래스 및 속성을 쿼리하지 않기 때문에 더 견고합니다.
우리는 ID, 클래스 이름 또는 data-testid
대신 요소의 텍스트 레이블로 쿼리할 것을 강력히 권장합니다.
필요한 경우, within
을 사용하여 페이지의 특정 영역 내에서 상호작용을 범위로 지정할 수 있습니다.
대개 레이블이 없는 div
와 같은 요소를 범위로 지정하게 될 것입니다. 이 경우에는 data-testid
선택자를 사용할 수 있습니다.
특징 테스트에서 axe 자동 접근성 테스트를 실행하기 위해 be_axe_clean
매처를 사용할 수 있습니다.
외부화된 내용
RSpec 테스트의 경우, 외부화된 내용에 대한 기대치는 번역과 일치하도록 동일한 외부화 메서드를 호출해야 합니다.
예를 들어, Ruby에서 _
메서드를 사용해야 합니다.
자세한 내용은 GitLab의 국제화 - 테스트 파일 (RSpec)를 참조하세요.
액션
가능한 경우, 아래와 같은 더 구체적인 액션을 사용하세요.
# 좋음
click_button _('리뷰 제출')
click_link _('UI 테스트 문서')
fill_in _('프로젝트 검색'), with: 'gitlab' # 텍스트로 텍스트 입력 채우기
select _('업데이트 날짜'), from: '정렬 기준' # 선택 입력에서 옵션 선택
check _('체크박스 레이블')
uncheck _('체크박스 레이블')
choose _('라디오 입력 레이블')
attach_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 _('리뷰 제출')
find_button _('리뷰 제출'), disabled: true
find_link _('UI 테스트 문서')
find_link _('UI 테스트 문서'), href: docs_url
find_field _('프로젝트 검색')
find_field _('프로젝트 검색'), with: 'gitlab' # 텍스트가 있는 입력 필드 찾기
find_field(_('프로젝트 검색'), disabled: true
find_field(_('체크박스 레이블'), checked: true
find_field _('체크박스 레이블'), unchecked: true
# 버튼, 링크 또는 필드가 아닌 요소를 찾을 때 허용됨
find_by_testid('element')
매처
가능한 경우, 아래와 같은 더 구체적인 매처를 사용하세요.
# 좋음
expect(page).to have_button _('리뷰 제출')
expect(page).to have_button _('리뷰 제출'), disabled: true
expect(page).to have_button _('알림'), class: 'is-checked' # "알림" GlToggle가 체크되었는지 확인
expect(page).to have_link _('UI 테스트 문서')
expect(page).to have_link _('UI 테스트 문서'), href: docs_url # 링크에 href가 있는지 확인
expect(page).to have_field _('프로젝트 검색')
expect(page).to have_field _('프로젝트 검색'), disabled: true
expect(page).to have_field(_('프로젝트 검색'), with: 'gitlab' # 입력 필드에 텍스트가 있는지 확인
expect(page).to have_checked_field _('체크박스 레이블')
expect(page).to have_unchecked_field _('라디오 입력 레이블')
expect(page).to have_select _('정렬 기준')
expect(page).to have_select(_('정렬 기준'), selected: '업데이트 날짜' # 옵션이 선택되었는지 확인
expect(page).to have_select(_('정렬 기준'), options: ['업데이트 날짜', '생성 날짜', '기한 날짜'] # 정확한 옵션 목록 확인
expect(page).to have_select(_('정렬 기준'), with_options: ['생성 날짜', '기한 날짜'] # 부분적인 옵션 목록 확인
expect(page).to have_text _('일부 단락 텍스트.')
expect(page).to have_text _('일부 단락 텍스트.'), exact: true # 정확한 일치 확인
expect(page).to have_current_path 'gitlab/gitlab-test/-/issues'
expect(page).to have_title _('찾을 수 없음')
# 위의 더 구체적인 매처가 불가능할 때 허용됨
expect(page).to have_css 'h2', text: '이슈 제목'
expect(page).to have_css 'p', text: '이슈 설명', exact: true
expect(page).to have_css '[data-testid="weight"]', text: 2
expect(page).to have_css '.atwho-view ul', visible: true
모달과 상호작용
within_modal
헬퍼를 사용하여 GitLab UI 모달과 상호작용하세요.
include Spec::Support::Helpers::ModalHelpers
within_modal do
expect(page).to have_link _('UI testing docs')
fill_in _('Search projects'), with: 'gitlab'
click_button 'Continue'
end
뿐만 아니라, 확인 모달을 수락하는 데만 필요한 accept_gl_confirm
을 사용할 수 있습니다.
이것은 window.confirm()
을 confirmAction
으로 마이그레이션할 때 유용합니다.
include Spec::Support::Helpers::ModalHelpers
accept_gl_confirm do
click_button 'Delete user'
end
예상 확인 메시지와 버튼 텍스트를 accept_gl_confirm
에 전달할 수도 있습니다.
include Spec::Support::Helpers::ModalHelpers
accept_gl_confirm('이 사용자를 삭제하시겠습니까?', button_text: '삭제') do
click_button 'Delete user'
end
기타 유용한 메서드
finder method를 사용하여 요소를 검색한 후, hover
와 같은 여러 element methods를 호출할 수 있습니다.
Capybara 테스트에도 accept_confirm
과 같은 여러 session methods가 있습니다.
기타 유용한 메서드는 아래와 같습니다:
refresh # 페이지 새로 고침
send_keys([:shift, 'i']) # Shift+I 키를 눌러 Issues 대시보드 페이지로 이동
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
Spring 프리로더를 통해 프로세스 8999에서 실행 중
실행 옵션: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}
현재 예제는 라이브 디버깅을 위해 일시 중지되었습니다.
현재 사용자 자격 증명은: user2 / 12345678
예제 실행을 재개하려면 아무 키나 누르십시오!
예제로 돌아오기!
.
34.51초 만에 완료되었습니다 (파일 로드에 0.76702초 소요됨).
1 예제, 0 실패
live_debug
는 JavaScript가 활성화된 스펙에서만 작동합니다.
가시적인 브라우저에서 :js
스펙 실행
WEBDRIVER_HEADLESS=0
으로 스펙을 실행하세요. 예를 들어:
WEBDRIVER_HEADLESS=0 bin/rspec some_spec.rb
테스트가 빠르게 완료되지만, 발생하는 일을 파악할 수 있습니다.
WEBDRIVER_HEADLESS=0
와 함께 live_debug
를 사용하면 열린 브라우저가 일시 중지되고 페이지가 다시 열리지 않습니다. 이는 디버깅 및 요소 검사에 사용할 수 있습니다.
또한 실행을 일시 중지하고 단계별로 진행하기 위해 byebug
또는 binding.pry
를 추가할 수 있습니다.
스크린샷
우리는 실패 시 자동으로 스크린샷을 찍기 위해 capybara-screenshot
젬을 사용합니다.
CI에서는 이러한 파일을 작업 아티팩트로 다운로드할 수 있습니다.
또한, 테스트 중 언제든지 아래 메서드를 추가하여 수동으로 스크린샷을 찍을 수 있습니다. 더 이상 필요하지 않을 때는 반드시 제거하세요! 더 많은 내용은 https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots를 참조하세요.
:js
사양에서 screenshot_and_save_page
를 추가하여 Capybara가 “보는” 내용을 스크린샷으로 찍고 페이지 소스를 저장하세요.
:js
사양에서 screenshot_and_open_image
를 추가하여 Capybara가 “보는” 내용을 스크린샷으로 찍고 이미지를 자동으로 엽니다.
이로 인해 생성된 HTML 덤프는 CSS가 결여되어 있습니다.
이러한 이유로 실제 애플리케이션과 많이 다르게 보입니다.
CSS를 추가하는 작은 해킹이 있어 디버깅을 더 쉽게 합니다.
빠른 단위 테스트
일부 클래스는 Rails와 잘 분리되어 있습니다.
이러한 경우 Rails 환경 및 Bundler의 :default
그룹의 젬 로딩으로 인한 오버헤드 없이 테스트할 수 있어야 합니다.
이런 경우, 테스트 파일에서 require 'fast_spec_helper'
를 사용할 수 있으며, 테스트는 정말 빠르게 실행됩니다.
- 젬 로딩이 생략됩니다.
- Rails 앱 부팅이 생략됩니다.
- GitLab Shell 및 Gitaly 설정이 생략됩니다.
- 테스트 리포지토리 설정이 생략됩니다.
fast_spec_helper
를 사용하는 테스트는 약 1초가 걸리며, 일반 spec_helper
의 경우 30초 이상 걸립니다.
fast_spec_helper
는 lib/
디렉터리에 위치한 클래스를 자동으로 로드하는 것도 지원합니다.
클래스나 모듈이 lib/
디렉터리의 코드만 사용하는 경우 종속성을 명시적으로 로드할 필요가 없습니다.
fast_spec_helper
는 또한 Rails 환경에서 일반적으로 사용되는 핵심 확장을 포함하여 모든 ActiveSupport 확장을 로드합니다.
일부 경우에는 젬을 사용하거나 종속성이 lib/
에 위치하지 않은 경우 require_dependency
를 사용하여 일부 종속성을 여전히 로드해야 할 수 있습니다.
예를 들어, 내부에서 re2
라이브러리를 사용하는 Gitlab::UntrustedRegexp
클래스를 호출하는 코드를 테스트하려면, 다음과 같이 해야 합니다:
-
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
를 사용하지 마세요. 이 경우 spec_helper
가 자동으로 로드됩니다.
fast_spec_helper 사양 유지 관리
fast_spec_helper
를 사용하는 모든 사양을 다양한 방법으로 실행할 수 있는 유틸리티 스크립트 scripts/run-fast-specs.sh
가 있습니다.
이 스크립트는 독립적으로 성공적으로 실행되지 않는 문제를 가진 fast_spec_helper
사양을 식별하는 데 유용합니다. 더 많은 내용은 스크립트를 참조하세요.
subject
및 let
변수
GitLab RSpec 스위트는 중복을 줄이기 위해 let
(및 그의 엄격한 비게으른 버전인 let!
) 변수를 광범위하게 사용했습니다. 그러나 이는 때때로 명확성의 대가를 치르므로, 앞으로의 사용을 위한 몇 가지 지침을 설정해야 합니다:
-
let!
변수는 인스턴스 변수보다 바람직합니다.let
변수는let!
변수보다 바람직합니다. 지역 변수는let
변수보다 바람직합니다. - 전체 사양 파일 전반에 걸쳐 중복을 줄이기 위해
let
을 사용하십시오. - 단일 테스트에서 사용되는 변수를 정의하기 위해
let
을 사용하지 마십시오. 테스트의it
블록 내에 지역 변수로 정의하십시오. - 더 깊게 중첩된
context
또는describe
블록에서만 사용되는let
변수를 최상위describe
블록 내에 정의하지 마십시오. 정의는 사용되는 곳에 최대한 가깝게 유지하십시오. - 하나의
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)
를 사용하여 이 작업을 구현하지 마십시오. 그렇게 하면, 이러한 훅이 데이터베이스 트랜잭션 외부에서 실행되기 때문에 데이터를 수동으로 정리해야 합니다.
대신, 아래의 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
, 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
는 allow
와 같은 스텁이 있는 팩토리와 사용할 수 없습니다. 그 이유는 let_it_be
가 before(:all)
블록 내에서 발생하며, RSpec은 before(:all)
내에서 스텁을 허용하지 않기 때문입니다.
자세한 내용은 이 문제를 참조하십시오.
해결하려면, let
을 사용하거나 팩토리를 스텁을 사용하지 않도록 변경하십시오.
시간 민감 테스트
ActiveSupport::Testing::TimeHelpers
는 시간 민감성을 검증하는 데 사용될 수 있습니다. 시간 민감성을 검증하거나 실행하는 모든 테스트는
일시적인 테스트 실패를 방지하기 위해 이러한 도우미를 사용해야 합니다.
예시:
it '기한 초과' do
issue = build(:issue, due_date: Date.tomorrow)
travel_to(3.days.from_now) do
expect(issue).to be_overdue
end
end
RSpec 도우미
:freeze_time
및 :time_travel_to
RSpec 메타데이터 태그 도우미를 사용하여
ActiveSupport::Testing::TimeHelpers
메서드로 전체 스펙을 래핑하는 데 필요한 보일러플레이트 코드를 줄일 수 있습니다.
describe '시간이 동결되어야 하는 스펙', :freeze_time do
it '시간을 동결한다' do
right_now = Time.now
expect(Time.now).to eq(right_now)
end
end
describe '특정 날짜 및/또는 시간으로 동결되어야 하는 스펙', time_travel_to: '2020-02-02 10:30:45 -0700' do
it '지정된 날짜와 시간으로 시간을 동결한다' do
expect(Time.now).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
end
end
작동 원리
에 대한 이 도우미들은 around(:each)
훅과
ActiveSupport::Testing::TimeHelpers
메서드의 블록 구문을 사용합니다:
around(:each) do |example|
freeze_time { example.run }
end
around(:each) do |example|
travel_to(date_or_time) { example.run }
end
예제가 실행되기 전에 생성된 어떤 객체(예: let_it_be
를 통해 생성된 객체)는 스펙 범위를 벗어납니다.
모든 것이 동결되어야 한다면 before :all
을 사용하여 설정을 캡슐화할 수 있습니다.
before :all do
freeze_time
end
after :all do
unfreeze_time
end
타임스탬프 절단
Active Record 타임스탬프는 Rails의 ActiveRecord::Timestamp
모듈에 의해 Time.now
를 사용하여 설정됩니다.
시간 정확도는 운영체제에 따라 다릅니다, 그리고 문서에서 언급했듯이, 소수 초를 포함할 수 있습니다.
Rails 모델이 데이터베이스에 저장될 때, 그들이 가진 어떤 타임스탬프는
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가 작성한 것입니다.
문제가 발생한 병합 요청 및 논의된 백엔드 페어링 세션 을 확인할 수 있습니다.
테스트에서의 기능 플래그
이 섹션은 기능 플래그로 개발하기로 이동되었습니다.
순수 테스트 환경
단일 GitLab 테스트로 실행되는 코드는 많은 데이터 항목에 접근하고 수정할 수 있습니다. 테스트가 실행되기 전에 신중한 준비가 없고 afterward 에 정리하지 않으면, 테스트가 후속 테스트의 동작에 영향을 미치는 방식으로 데이터를 변경할 수 있습니다. 이는 어떤 비용을 들여서라도 피해야 합니다! 다행히도, 기존의 테스트 프레임워크는 대부분의 경우를 이미 처리합니다.
테스트 환경이 오염되면, 일반적인 결과는 불안정한 테스트입니다. 오염은 종종 순서 종속성으로 나타납니다: 스펙 A를 실행한 후 스펙 B를 실행하면 안정적으로 실패하지만, 스펙 B를 실행한 후 스펙 A를 실행하면 안정적으로 성공합니다. 이러한 경우, rspec --bisect
(또는 스펙 파일의 수동 쌍별 이분법)를 사용하여 어떤 스펙이 문제인지 확인할 수 있습니다. 문제를 해결하려면 테스트 스위트가 환경을 얼마나 잘 유지하는지 이해해야 합니다. 각 데이터 저장소에 대해 더 알아보려면 계속 읽어보세요!
SQL 데이터베이스
이는 database_cleaner
젬에 의해 관리됩니다. 각 스펙은 트랜잭션으로 둘러싸여 있으며, 테스트가 완료되면 롤백됩니다. 특정 스펙은 대신 완료 후 모든 테이블에 대해 DELETE FROM
쿼리를 실행합니다. 이는 여러 데이터베이스 연결에서 생성된 행을 볼 수 있게 하며, 브라우저에서 실행되는 스펙이나 마이그레이션 스펙 등에서 중요합니다.
이러한 전략을 사용하면 잘 알려진 TRUNCATE TABLES
접근 방식 대신 기본 키 및 기타 시퀀스가 리셋되지 않는다는 결과가 있습니다. 따라서 스펙 A에서 프로젝트를 생성한 후 스펙 B에서 프로젝트를 생성하면 첫 번째는 id=1
이고 두 번째는 id=2
가 됩니다.
이 의미는 스펙이 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_might_not_need_inline
특성은
Sidekiq 인라인 모드가 가짜 모드로 변경되었을 때
실제로 작업을 처리하기 위해 Sidekiq가 필요한 모든 테스트에 추가되었습니다.
이 특성이 있는 테스트는 Sidekiq가 작업을 처리하는 데 의존하지 않도록 수정되거나
백그라운드 작업의 처리가 필요/예상되는 경우
:sidekiq_might_not_need_inline
특성을 :sidekiq_inline
으로 업데이트해야 합니다.
perform_enqueued_jobs
의 사용은 지연된 메일 배달 테스트에만 유용합니다.
우리의 Sidekiq 작업자는 ApplicationJob
/ ActiveJob::Base
를 상속받지 않기 때문입니다.
DNS
DNS 요청은 테스트 스위트에서 보편적으로 스텁 처리됩니다
(현재 !22368 기준)
DNS는 개발자의 로컬 네트워크에 따라 문제를 일으킬 수 있습니다.
DNS 스텁을 우회해야 하는 경우
spec/support/dns.rb
에 있는 RSpec 레이블을 테스트에 적용할 수 있습니다.
예시는 다음과 같습니다:
it "정말로 Prometheus에 연결합니다", :permit_dns do
보다 구체적인 제어가 필요한 경우
DNS 차단은 spec/support/helpers/dns_helpers.rb
에 구현되어 있으며
이 메서드는 다른 곳에서도 호출할 수 있습니다.
속도 제한
속도 제한은 테스트 스위트에서 활성화되어 있습니다.
속도 제한은 :js
특성을 사용하는 기능 사양에서 발생할 수 있습니다.
대부분의 경우, 속도 제한을 발생시키지 않도록 하려면
사양에 :clean_gitlab_redis_rate_limiting
특성을 표시하면 됩니다.
이 특성은 사양 간에 Redis 캐시에 저장된 속도 제한 데이터를 지웁니다.
단일 테스트에서 속도 제한을 유발하는 경우,
: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
특성은 파이프라인의 실행 시간을 줄이기 위해 각 컨텍스트의 시작과 끝에서만 인덱스를 생성하고 삭제하도록 추가되었습니다. Elasticsearch delete by query API를 사용하여 모든 인덱스(마이그레이션 인덱스 제외)에서 예제 간 데이터를 삭제하여 깨끗한 인덱스를 보장합니다.
:elastic_clean
특성은 예제 간에 인덱스를 생성하고 삭제하여 깨끗한 인덱스를 보장합니다. 이렇게 하면 테스트가 비필수 데이터로 오염되지 않습니다. :elastic
또는 :elastic_delete_by_query
특성을 사용할 때 문제가 발생하면 대신 :elastic_clean
을 사용하세요. :elastic_clean
은 다른 특성보다 현저히 느리므로 드물게 사용해야 합니다.
Elasticsearch 로직에 대한 대부분의 테스트는 다음과 관련이 있습니다:
- PostgreSQL에 데이터를 생성하고 Elasticsearch에 인덱싱되기를 기다립니다.
- 해당 데이터를 검색합니다.
- 테스트가 예상한 결과를 제공하는지 확인합니다.
예외적으로, 특정 인덱스의 개별 레코드가 아닌 구조적 변화를 확인하는 경우가 있습니다.
참고:
Elasticsearch 인덱싱은 Gitlab::Redis::SharedState
를 사용합니다. 따라서 Elasticsearch 특성은 동적으로 :clean_gitlab_redis_shared_state
특성을 사용합니다. :clean_gitlab_redis_shared_state
를 수동으로 추가할 필요는 없습니다.
Elasticsearch를 사용하는 사양에서는 다음이 필요합니다:
- PostgreSQL에 데이터를 생성한 다음 Elasticsearch에 인덱싱합니다.
- Elasticsearch에 대한 애플리케이션 설정을 활성화합니다(기본적으로 비활성화됨).
이를 위해 다음을 사용합니다:
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
또한, 비동기적 특성을 극복하기 위해 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
을
모킹할 때 예외를 포착하기가 어려울 수 있습니다.
타입 체크로 인한 런타임 오류를 포착하려면, Gitlab::Tracking#event
에 대한 호출을 확인하는
expect_snowplow_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 스키마 매처는
JSON 스키마에 대한 Snowplow 컨텍스트를 테스트하여 유효성 검사 오류를 줄이는 데 도움이 됩니다.
스키마 매처는 다음 매개변수를 허용합니다:
schema path
context
스키마 매처 스펙을 추가하려면:
-
Iglu 리포지토리에 새 스키마를 추가한 후,
동일한 스키마를spec/fixtures/product_intelligence/
디렉터리에 복사합니다. - 복사된 스키마에서
"$schema"
키와 값을 제거합니다.
스펙에서는 필요하지 않으며, 키를 유지하면 스펙이 실패하며 URL에서 스키마를 찾으려 합니다. -
다음 스니펫을 사용하여 스키마 매처를 호출합니다:
match_snowplow_context_schema(schema_path: '<filename from step 1>', context: <Context Hash> )
테이블 기반 / 매개변수화된 테스트
이 스타일의 테스트는 하나의 코드 조각을 포괄적인 입력 범위를 통해 실행하는 데 사용됩니다.
테스트 사례를 한 번 지정하고, 입력 및 각 출력에 대한 예상 결과의 테이블을 함께 지정함으로써,
테스트를 더 읽기 쉽고 간결하게 만들 수 있습니다.
우리는 RSpec::Parameterized
gem을 사용합니다. 테이블 구문을 사용하고 다양한 입력에 대한 Ruby 동등성을 확인하는 짧은 예는 다음과 같습니다:
describe "#==" do
using RSpec::Parameterized::TableSyntax
let(:one) { 1 }
let(:two) { 2 }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
ref(:one) | ref(:one) | true # let 변수를 참조하려면 `ref`를 사용해야 합니다.
ref(:one) | ref(:two) | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
테이블 기반 테스트를 만든 후, 다음과 같은 오류를 볼 수 있습니다:
NoMethodError:
undefined method `to_params'
param_sets = extracted.is_a?(Array) ? extracted : extracted.to_params
^^^^^^^^^^
Did you mean? to_param
이는 스펙 파일에 using RSpec::Parameterized::TableSyntax
라인을 추가해야 함을 의미합니다.
경고:
where
블록에서는 단순한 값만 입력으로 사용해야 합니다. 프로시, 상태가 있는 객체,
FactoryBot으로 생성된 객체 및 유사한 항목을 사용하면
예상치 못한 결과를 초래할 수 있습니다.
프로메테우스 테스트
프로메테우스 메트릭은 하나의 테스트 실행에서 다른 테스트 실행으로 유지될 수 있습니다. 각 예제 전에 메트릭이 재설정되도록 하려면 RSpec 테스트에 :prometheus
태그를 추가하세요.
매처
의도 를 명확히 하거나 RSpec 기대치를 숨기기 위해 사용자 지정 매처를 만들어야 합니다. 이러한 매처는 spec/support/matchers/
아래에 배치해야 합니다. 매처는 특정 유형의 사양(예: 기능 또는 요청)에만 적용되는 경우 하위 폴더에 배치할 수 있지만 여러 유형의 사양에 적용되는 경우에는 배치하지 않아야 합니다.
be_like_time
데이터베이스에서 반환된 시간은 Ruby의 시간 객체에서 정밀도가 다를 수 있으므로, 사양에서 비교할 때 유연한 허용 오차가 필요합니다.
PostgreSQL의 시간 및 타임스탬프 유형은 1 마이크로초의 해상도를 가지고 있습니다. 그러나 Ruby 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_http_status
및 expect(response.status).to
보다 have_gitlab_http_status
를 선호하세요. 왜냐하면 전자는 상태가 일치하지 않을 때 응답 본문도 보여줄 수 있기 때문입니다. 이는 테스트가 깨지기 시작할 때, 소스를 수정하지 않고도 왜 그런지 알고 싶을 때 매우 유용합니다.
특히 500 내부 서버 오류를 나타낼 때 유용합니다.
숫자 표현 206
보다 :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/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/
아래에 두어야 합니다.
헬퍼는 특정 유형의 스펙에만 적용되는 경우 하위 폴더에 두어도 되지만 여러 유형의 스펙에 적용되는 경우에는 그렇지 않아야 합니다.
헬퍼는 Rails 네이밍/네임스페이스 규칙을 따라야 하며,
spec/support/helpers/
가 루트입니다. 예를 들어
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
# 좋음, 특정 스펙에 포함
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
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 end
- 팩토리는
ActiveRecord
객체에 제한되지 않아도 됩니다.
예제 보기. - 팩토리와 그 트레이트는 모든 모델 사양에서 실행되는 공유 사양으로 검증된 유효한 객체를 생성해야 합니다.
- 팩토리에서
skip_callback
의 사용을 피하세요.
자세한 내용은 이슈 #247865를 참조하세요.
Fixtures
모든 피xtures는 spec/fixtures/
아래에 배치되어야 합니다.
Repositories
병합 요청을 병합하는 것과 같은 기능을 테스트하려면 테스트 환경에 특정 상태의 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
이 코드는 기본 권한과 지정된 내용을 가진 두 개의 파일이 포함된 리포지토리를 생성합니다.
Configuration
RSpec 구성 파일은 RSpec 구성을 변경하는 파일입니다 (예: RSpec.configure do |config|
블록). 이 파일들은 spec/support/
아래에 배치되어야 합니다.
각 파일은 spec/support/capybara.rb
또는 spec/support/carrierwave.rb
와 같이 특정 도메인과 관련이 있어야 합니다.
도움 모듈이 특정 종류의 스펙에만 적용된다면, config.include
호출에 수식어를 추가해야 합니다. 예를 들어, spec/support/helpers/cycle_analytics_helpers.rb
가 :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
파일에서 사용됩니다. spec/fast_spec_helper.rb
파일에 대한 자세한 내용은 빠른 단위 테스트를 참조하세요.
Test environment logging
테스트 환경에 대한 서비스는 테스트가 실행될 때 자동으로 구성되고 시작됩니다. 여기에는 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 인덱서 설정 중...
GitLab Elasticsearch 인덱서가 26.514623초 만에 설정되었습니다...
이 정보는 로컬에서 실행되거나 수행할 작업이 없을 때 생략됩니다. 이러한 메시지를 항상 보고 싶다면 다음 환경 변수를 설정하세요:
GITLAB_TESTING_LOG_LEVEL=debug