- 클래스 및 모듈 네이밍
- 테스트에 대한 테스트 케이스 링크
- 테스트 네이밍
- API를 UI보다 선호
- 불필요한 기대값 피하기
- 연이어 나오는 기대값에는
aggregate_failures
를 선호 expect do ... raise_error
블록에서 여러 동작을 피하세요- 테스트를 여러 파일로 분할하기를 선호하세요
let
변수 대 인스턴스 변수before(:context)
및after
후크에서 UI 사용 제한- 브라우저에 로그인된 채로 테스트가 종료되지 않도록 보장
- 관리자 액세스가 필요한 테스트에 태그 지정
ProjectPush
대신Commit
리소스 선호- 요소를 흐리게 하는 우선적인 방법
-
expect
문이 효율적으로 대기하도록 보장 puts
대신 로거(logger) 사용
엔드 투 엔드 테스트 베스트 프랙티스
이것은 테스팅 가이드에서 찾을 수 있는 베스트 프랙티스를 개정한 것입니다.
클래스 및 모듈 네이밍
QA 프레임워크는 클래스와 모듈을 자동으로 로드하기 위해 Zeitwerk를 사용합니다. 기본 Zeitwerk inflector는 snake_cased 파일 이름을 PascalCased 모듈 또는 클래스 이름으로 변환합니다. 이 패턴을 유지하여 인플렉션을 수동으로 유지하는 것을 피하는 것이 좋습니다.
사용자 정의 인플렉션 로직이 필요한 경우 qa.rb 파일에 사용자 정의 인플렉터가 loader.inflector.inflect
메소드 호출로 추가됩니다.
테스트에 대한 테스트 케이스 링크
모든 테스트는 GitLab 프로젝트 테스트 케이스에 해당하는 테스트 케이스 및 결과 이슈와 품질 테스트 케이스 프로젝트에 해당하는 결과 이슈를 가져야 합니다.
테스트 케이스 이슈가 아직 없는 경우 GitLab 팀 멤버는 GitLab 프로젝트의 CI/CD > 테스트 케이스 페이지에서 새로운 테스트 케이스를 생성할 수 있습니다. 테스트 케이스 URL이 코드에서 테스트에 연결되면, 테스트가 보고 기능을 사용하는 파이프라인에서 실행될 때 report-results
스크립트가 자동으로 테스트 케이스 및 결과 이슈를 업데이트합니다.
결과 이슈가 아직 없는 경우 report-results
스크립트가 자동으로 결과 이슈를 생성하고 해당 테스트 케이스에 연결합니다.
테스트 케이스를 코드에서 테스트에 연결하려면 수동으로 testcase
RSpec 메타데이터 태그를 추가해야 합니다.
대부분의 경우, 단일 테스트가 단일 테스트 케이스와 연관되어 있습니다.
예를 들어:
RSpec.describe 'Stage' do
describe '테스트되는 기능의 일반 설명' do
it '테스트 이름', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/:test_case_id' do
...
end
it '다른 테스트', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/:another_test_case_id' do
...
end
end
end
공유 테스트의 경우
대부분의 테스트는 spec
파일의 한 줄로 정의되기 때문에 해당 테스트는 testcase
태그를 통해 단일 테스트 케이스에 연결될 수 있습니다.
그러나 일부 테스트는 spec
파일의 한 줄과 테스트 케이스 간에 일대일 대응 관계를 갖지 않습니다.
일부 테스트는 병렬화된 테스트, 템플릿화된 테스트, 둘 이상의 예제를 포함하는 공유 예제의 경우입니다.
이러한 경우에는 다른 방법으로 테스트 케이스 링크를 포함해야 합니다.
예를 들어, qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
에 있는 공유 예제에는 두 개의 테스트가 있습니다:
RSpec.shared_examples 'unselected maintainer' do |testcase|
it '사용자가 푸시에 실패함', testcase: testcase do
...
end
end
RSpec.shared_examples 'selected developer' do |testcase|
it '사용자가 푸시하고 병합함', testcase: testcase do
...
end
end
공유 예제를 포함하는 다음 테스트를 고려해 보십시오:
RSpec.describe 'Create' do
describe 'Restricted protected branch push and merge' do
context '프로젝트에서 푸시와 병합이 제한된 보호된 브랜치가 하나의 사용자만 허용되는 경우' do
...
it_behaves_like 'unselected maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347775'
it_behaves_like 'selected developer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347774'
end
context '그룹에서 푸시와 병합이 제한된 보호된 브랜치가 하나의 사용자만 허용되는 경우' do
...
it_behaves_like 'unselected maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347772'
it_behaves_like 'selected developer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347773'
end
end
end
각 공유 예제에 대해 두 개의 연관 테스트 케이스를 생성하는 것이 좋습니다.
테스트 네이밍
테스트 이름은 테스트의 목적을 정의하는 읽기 쉬운 문장을 형성해야 합니다. 테스팅 가이드에서는 Thoughtbot 테스트 스타일 가이드를 확장합니다. 이 페이지는 https://www.betterspecs.org/ 및 the RSpec 네이밍 가이드와 함께 지침을 명확히 합니다.
추천 접근 방식
다음 블록은 Plan wiki content creation in a project adds a home page
라는 이름의 테스트를 생성합니다.
# `RSpec.describe`는 커버되는 DevOps Stage입니다.
RSpec.describe 'Plan', product_group: :knowledge do
# `describe`는 테스트되는 기능입니다.
describe 'wiki content creation' do
# `context`는 커버되는 조건을 제공합니다.
context 'in a project'
# `it`은 테스트의 예상 결과를 정의합니다.
it 'adds a home page'
...
end
...
end
...
end
end
- 각
describe
,context
,it
블록은 짧은 설명이 함께해야합니다. - 설명을 가능한 한 간결하게 유지해야 합니다.
- 긴 설명이나 여러 조건문은 분할되어야 할 지표일 수 있습니다(추가
context
블록). - Documentation Style Guide는 간결하게 작성하고 능동태로 작성하는 방법에 대한 권장사항을 제공합니다.
- 긴 설명이나 여러 조건문은 분할되어야 할 지표일 수 있습니다(추가
- 가장 바깥쪽
Rspec.describe
블록은 DevOps stage 이름이어야 합니다. -
Rspec.describe
블록 내부에는 테스트되는 기능의 이름이 있는describe
블록이 있습니다. - 선택적
context
블록은 테스트되는 조건을 정의합니다.-
context
블록 설명은 RuboCop 규칙과 일치하기 위해when
,with
,without
,for
,and
,on
,in
,as
, 또는if
로 시작해야 합니다.
-
API를 UI보다 선호
엔드투엔드 테스트 프레임워크는 필요에 따라 리소스를 개별적으로 제작할 수 있는 능력이 있습니다. 가능한 경우 리소스는 API를 통해 제작해야 합니다.
테스트에 필요한 리소스를 API를 통해 제작함으로써 시간과 비용을 절약할 수 있습니다.
불필요한 기대값 피하기
테스트를 간소화하기 위해 중요한 것만을 테스트하는 것이 중요합니다.
테스트해야 하는 내용과 관련 없는 expect()
문이 추가되지 않도록 해야 합니다.
예시:
#=> 좋음
Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
expect(menu).to be_signed_in
end
#=> 나쁨
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform do |menu|
expect(menu).to be_signed_in
expect(page).to have_content(user.name) #=> 로그인 여부가 이미 확인되었으므로 중복됨.
expect(menu).to have_element(:nav_bar) #=> 대부분 불필요할 확률이 높음. 이미 하위 단계에서 확인되었으므로 이를 검증하는 것은 테스트에 필요하지 않음.
end
#=> 좋음
issue = create(:issue, name: 'issue-name')
Project::Issues::Index.perform do |index|
expect(index).to have_issue(issue)
end
#=> 나쁨
issue = create(:issue, name: 'issue-name')
Project::Issues::Index.perform do |index|
expect(index).to have_issue(issue)
expect(page).to have_content(issue.name) #=> 문제가 이미 윗 줄에서 확인되었으므로 페이지 내용 확인은 중복됨.
end
연이어 나오는 기대값에는 aggregate_failures
를 선호
여러 개의 기대값이 테스트 케이스 내에 있어야 하는 경우 aggregate_failures
를 사용하는 것이 좋습니다.
이를 통해 실패하는 모든 기대값을 한눈에 볼 수 있으며 첫 번째 실패에 의해 테스트가 중단되는 것보다 나은 방법입니다.
예시:
#=> 좋음
Page::Search::Results.perform do |search|
search.switch_to_code
aggregate_failures 'testing search results' do
expect(search).to have_file_in_project(template[:file_name], project.name)
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
#=> 나쁨
Page::Search::Results.perform do |search|
search.switch_to_code
expect(search).to have_file_in_project(template[:file_name], project.name)
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
여러 개의 기대값이 문장으로 구분될 경우 :aggregate_failures
메타데이터를 예제에 첨부하세요.
#=> 좋음
it 'searches', :aggregate_failures do
Page::Search::Results.perform do |search|
expect(search).to have_file_in_project(template[:file_name], project.name)
search.switch_to_code
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
#=> 나쁨
it 'searches' do
Page::Search::Results.perform do |search|
expect(search).to have_file_in_project(template[:file_name], project.name)
search.switch_to_code
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
expect do ... raise_error
블록에서 여러 동작을 피하세요
여러 동작을 하나의 expect do ... end.not_to raise_error
또는 expect do ... end.to raise_error
블록으로 묶으면 로그가 출력되는 방식으로 인해 실제 실패의 원인을 파악하기 어려울 수 있습니다. 중요한 정보가 일부 생략되거나 아예 누락될 수 있습니다.
예시로 테스트 내에서 expect_owner_permissions_allow_delete_issue
와 같은 개인 메소드에 여러 동작과 기대값을 캡슐화하면 다음과 같습니다.
it "has Owner role with Owner permissions" do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Owner')
end
expect_owner_permissions_allow_delete_issue
end
그리고 메소드 내부에서:
#=> 좋음
def expect_owner_permissions_allow_delete_issue
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end
#=> 나쁨
def expect_owner_permissions_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end.not_to raise_error
end
테스트를 여러 파일로 분할하기를 선호하세요
저희 프레임워크에는 여러 명령을 병렬로 실행하는 몇 가지 병렬화 메커니즘이 포함되어 있습니다.
그러나 테스트는 특정 파일이 아닌 테스트/예제로 병렬화되기 때문에 기존 파일에 새 테스트를 추가해도 더 큰 병렬화를 달성할 수 없습니다.
그러나 기존 파일에 새 테스트를 추가하는데 다른 이유가 있을 수 있습니다.
예를 들어, 테스트가 병렬화하기 어려운 비용이 많이 드는 상태를 공유하는 경우, 해당 설정을 한 번만 수행하는 효율적인 방법일 수 있습니다.
요약하면:
- 하다: 테스트를 별도의 파일로 분할하되, 테스트가 공유 비용이 많이든 상황을 고려하세요.
- 하지 마라: 병렬화에 미치는 영향을 고려하지 않고 새 테스트를 기존 파일에 추가하세요.
let
변수 대 인스턴스 변수
기본적으로 let
또는 인스턴스 변수를 사용할 때 테스트 최적의 방법을 따르세요. 그러나 엔드투엔드 테스트에서는 리소스 생성과 같은 설정이 비용이 많이 들기 때문에 let
을 사용하면 각 예제마다 리소스가 생성될 것입니다. 리소스가 여러 예제에서 공유될 수 있다면 해당 리소스를 만들 때 let
대신 before(:all)
에서 인스턴스 변수를 사용하여 실행 시간을 절약하세요. 여러 예제에서 공유할 수 없는 경우 let
을 사용하세요.
before(:context)
및 after
후크에서 UI 사용 제한
before(:context)
후크를 사용하여 API 호출, 비-UI 작업 또는 로그인과 같은 기본적인 UI 작업을 수행하는 것을 제한하세요.
capybara-screenshot
라이브러리를 사용하여 실패 시 자동으로 스크린샷을 저장합니다.
capybara-screenshot
은 RSpec의 after
후크에서 스크린샷을 저장합니다. 만약 before(:context)
에서 실패가 발생하면 after
후크가 호출되지 않습니다 따라서 스크린샷이 저장되지 않습니다.
따라서 before(:context)
의 사용을 스크린샷이 필요하지 않은 작업에만 제한해야 합니다.
비슷하게 after
후크는 UI 작업만을 사용해야 합니다. 테스트 파일의 after
후크에서 UI 작업이 실행되면 스크린샷이 적절한 순간에 캡처되지 않아 문제를 파악하는 데 어려움을 겪을 수 있습니다.
브라우저에 로그인된 채로 테스트가 종료되지 않도록 보장
모든 테스트는 테스트가 시작될 때 로그인할 수 있다고 예상합니다.
예: 이슈 #34736 참조
이상적으로는 after(:context)
(또는 before(:context)
) 블록에서 수행된 작업은 API를 사용하여 수행합니다. 사용자 인터페이스를 통해 수행해야 하는 경우(예: API 기능이 없는 경우), 블록의 끝에서 로그아웃을 반드시 수행하세요.
after(:all) do
login unless Page::Main::Menu.perform(&:signed_in?)
# 로그인된 상태에서 작업 수행
Page::Main::Menu.perform(&:sign_out)
end
관리자 액세스가 필요한 테스트에 태그 지정
관리자 액세스가 필요한 테스트는 제품 환경에서 실행되지 않습니다.
관리자 액세스가 필요한 새로운 테스트를 추가할 때는 테스트가 제품 및 기타 실행 환경에서 실행되지 않도록 RSpec 메타데이터 :requires_admin
을 적용하세요.
로컬에서 테스트를 실행하거나 파이프라인을 구성할 때 환경 변수 QA_CAN_TEST_ADMIN_FEATURES
를 false
로 설정하여 :requires_admin
태그가 지정된 테스트를 건너뛸 수 있습니다.
참고:
테스트에서 관리자 액세스가 필요한 유일한 동작이 피쳐 플래그를 토글하는 경우 feature_flag
태그를 사용하세요. 자세한 내용은 피쳐 플래그로 테스트를 참조하세요.
ProjectPush
대신 Commit
리소스 선호
API 사용을 준수하면서 가능한 경우 Commit
리소스를 사용하세요.
ProjectPush
는 Git 명령줄 인터페이스(CLI)의 원시 쉘 명령을 사용하고 Commit
리소스는 HTTP 요청을 만듭니다.
# Commit 리소스 사용
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.commit_message = 'Initial commit'
commit.add_files([
{ file_path: 'README.md', content: 'Hello, GitLab' }
])
end
# ProjectPush 사용
Resource::Repository::ProjectPush.fabricate! do |push|
push.commit_message = 'Initial commit'
push.file_name = 'README.md'
push.file_content = 'Hello, GitLab'
end
ProjectPush
를 사용해야하는 몇 가지 예외 상황은 테스트가 SSH 통합을 테스트하거나 Git CLI를 사용해야하는 경우입니다.
요소를 흐리게 하는 우선적인 방법
요소를 흐리게 하려면 테스트 상태를 변경하지 않는 다른 요소를 선택하는 것이 우선입니다.
일부 드롭다운과 함께 발생할 수있는 것과 같이 페이지 요소를 차단하는 마스크가 있는 경우 WebDriver
의 기본 마우스 이벤트를 사용하여 요소의 좌표를 클릭 이벤트를 모방하십시오. 다음 방법을 사용하세요: click_element_coordinates
.
텍스트 입력 상자 및 드롭다운과 같은 요소를 흐리게 하는 데 body
를 클릭하는 것은 지양하십시오. 이렇게하면 뷰포트의 중앙을 클릭하게됩니다. 이 작업은 테스트 상태를 의도치 않게 변경하고 실패할 수 있습니다.
# 다른 요소를 클릭하여 입력을 흐리게 하는 방법
def add_issue_to_epic(issue_url)
find_element(:issue_actions_split_button).find('button', text: 'Add an issue').click
fill_element(:add_issue_input, issue_url)
# 제목을 클릭하여 입력을 흐릅니다
click_element(:title)
click_element(:add_issue_button)
end
# 마스크/오버레이가있는 경우 기본 마우스 클릭 이벤트 사용
click_element_coordinates(:title)
expect
문이 효율적으로 대기하도록 보장
일반적으로 expect
문을 사용하여 무언가가 기대한대로 있는지 확인합니다. 예를 들어:
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).to have_job('a_job')
end
대기가 필요한 기대 사항에 대해 eventually_
매처 사용
대기가 필요한 경우 eventually_
매처를 사용하여 명확한 대기 시간 정의를 사용하십시오.
Eventually
매처는 다음과 같은 이름 패턴을 사용합니다: eventually_${rspec_matcher_name}
. 이들은 eventually_matcher.rb에서 정의됩니다.
expect { async_value }.to eventually_eq(value).within(max_duration: 120, max_attempts: 60, reload_page: page)
부정 가능한 매처를 만들어 expect
확인 속도 향상
그러나 때로는 무언가가 기대한대로 아닌지 확인하고 싶을 수도 있습니다. 다시 말해, 어떤 것이 없는지 확인하려고 합니다. 단위 테스트 및 피쳐 스펙에서 we commonly us 릿che template (_)의 not_to
를 사용할 수 있습니다. ‘의 내장 matchers는 부정 가능하며, Capybara의도 마찬가지입니다.
except(page).not_to have_text('hidden')
except(page).to have_no_text('hidden')
유감으로는 테스트 프레임워크에 추가 된 서술 메서드에 대해 이렇게 자동으로 적용되지는 않습니다. 테스트 프레임워크에 추가 된 서술 메서드에 대해 나만의 부정 가능한 매처를 만들어야합니다.
최초 예는 Page::Project::Pipeline::Show
페이지 객체의 has_job?
서술 메서드에서 파생 된have_job
매처를 사용합니다. 부정 가능한 매처를 만들려면 부정적인 경우에 has_no_job?
를 사용하세요.
RSpec::Matchers.define :have_job do |job_name|
match do |page_object|
page_object.has_job?(job_name)
end
match_when_negated do |page_object|
page_object.has_no_job?(job_name)
end
end
그런 다음 다음 예제의 두 가지 expect
문은 같은 의미입니다.
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job')
expect(pipeline).to have_no_job('a_job')
end
사용자 지정 매처 추가에 대한 실제 예제를 보려면이 머지 리퀘스트를 확인하십시오.
우리는 qa/spec/support/matchers
에서 사용자 지정 부정 가능한 매처를 생성하고 있습니다.
참고:
테스트 프레임워크에 추가 한 서술 메서드에 대해서만 부정 가능한 매처를 작성해야하며 not_to
를 사용하는 경우에만 필요하지만 코드 가독성을 높입니다.
Negatable matchers의 필요성
다음 코드를 고려해보지만, have_job
에 대한 사용자 정의 가능한 부정형 매처가 없다고 가정해봅시다.
# 나쁜 예
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job')
end
이 문장이 통과하려면, have_job('a_job')
이 false
를 반환하여 not_to
가 이를 부정할 수 있어야 합니다.
문제는 have_job('a_job')
이 'a job'
이 나타날 때까지 최대 열 초를 기다린 후 false
을 반환해야 한다는 것입니다. 기대 조건 하에서 이 테스트는 필요한 것보다 열 초 더 오래 걸릴 것입니다.
대신, 우리는 대기 시간을 강제할 수 있습니다:
# 나쁘진 않지만 잠재적으로 불안정한 예
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job', wait: 0)
end
문제는 'a_job'
이 존재하고 사라질 때까지 기다리는 동안, 이 문장은 실패할 것입니다.
사용자 정의 가능한 부정형 matcher를 생성하면 이러한 문제가 없습니다. 왜냐하면 has_no_job?
예측 메서드가 사용되어 작업이 사라질 때 까지 필요한만큼만 기다리기 때문입니다.
마지막으로, 사용자 정의 가능한 매처는 have_no_*
형태의 매처를 사용하는 것보다 우선합니다. 왜냐하면 not_to
를 사용하여 매처를 부정하는 것은 일반적이고 익숙한 실천법이기 때문입니다. 우리가 부정 가능한 매처를 추가하여 이 실천법을 지원한다면, 후속 테스트 작성자가 효율적인 테스트를 쉽게 작성할 수 있게 됩니다.
puts
대신 로거(logger) 사용
우리는 현재 GitLab QA 애플리케이션과 종단간 테스트에서 로그를 처리하기 위해 Rails 로거(logger)
를 사용하고 있습니다.
이는 puts
와 비교했을 때 추가 기능을 제공합니다. 예를 들어:
- 로깅 레벨을 지정할 수 있는 기능.
- 유사한 로그에 태그를 지정할 수 있는 기능.
- 로그 메시지 자동 형식화 기능.