- 클래스와 모듈 네이밍
- 테스트를 해당 테스트 케이스에 연결하기
- 테스트 네이밍
- API를 UI보다 선호
- 불필요한 기대치를 피하세요
- 연이어 기대하는 경우
aggregate_failures
를 선호 expect do ... raise_error
블록에서 여러 동작 피하기- 테스트를 여러 파일로 분할하는 것을 선호
let
변수 대 인스턴스 변수before(:context)
및after
후크에서 UI 사용 제한- 브라우저에 로그인된 상태를 유지하지 않도록 테스트 보장
- 관리자 액세스가 필요한 테스트에 태그 지정
ProjectPush
대신Commit
리소스를 선호- 요소를 흐리게 하는 선호 방법
-
expect
문이 효율적으로 대기하도록 보장 puts
대신에 로거를 사용하세요
엔드투엔드 테스트 베스트 프랙티스
이것은 테스팅 가이드에서 찾을 수 있는 베스트 프랙티스의 맞춤형 확장판입니다.
클래스와 모듈 네이밍
QA 프레임워크는 클래스 및 모듈 자동로딩을 위해 Zeitwerk를 사용합니다. 기본 Zeitwerk inflector는 snake_cased 파일 이름을 PascalCased 모듈 또는 클래스 이름으로 변환합니다. 매뉴얼으로 인플렉션을 유지보수하는 것을 피하기 위해 이 패턴을 따르는 것이 좋습니다.
사용자 정의 인플렉션 로직이 필요한 경우, loader.inflector.inflect
메서드를 호출하는 qa.rb 파일에 사용자 정의 인플렉터를 추가해야 합니다.
테스트를 해당 테스트 케이스에 연결하기
모든 테스트는 GitLab 프로젝트 테스트 케이스에 해당하는 테스트 케이스와 품질 테스트 케이스 프로젝트에서 결과 이슈를 가져야 합니다.
테스트 케이스 이슈가 아직 존재하지 않은 경우, GitLab 팀 구성원은 GitLab 프로젝트의 CI/CD > Test cases 페이지에서 새로운 테스트 케이스를 만들 수 있습니다. 테스트 케이스 URL이 코드의 테스트에 연결되면, 보고가 활성화된 파이프라인에서 테스트가 실행될 때 report-results
스크립트가 자동으로 테스트 케이스와 결과 이슈를 업데이트합니다.
결과 이슈가 아직 존재하지 않은 경우, report-results
스크립트가 자동으로 결과 이슈를 생성하고 해당 테스트 케이스에 연결합니다.
코드 내의 테스트 케이스를 테스트 케이스에 연결하려면 매뉴얼으로 testcase
RSpec 메타데이터 태그를 추가해야 합니다. 대부분의 경우, 단일 테스트가 단일 테스트 케이스와 연관되어 있습니다.
예시:
RSpec.describe 'Stage' do
describe 'General description of the feature under test' do
it 'test name', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/:test_case_id' do
...
end
it 'another test', 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 'user fails to push', testcase: testcase do
...
end
end
RSpec.shared_examples 'selected developer' do |testcase|
it 'user pushes and merges', testcase: testcase do
...
end
end
공유 예제를 포함하는 다음 테스트를 고려해보세요.
RSpec.describe 'Create' do
describe 'Restricted protected branch push and merge' do
context 'when only one user is allowed to merge and push to a protected branch' 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 'when only one group is allowed to merge and push to a protected branch' 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/ 및 RSpec 네이밍 가이드와의 입력을 명확히합니다.
권장 접근 방식
다음 블록은 Plan wiki content creation in a project adds a home page
라는 이름의 테스트를 생성합니다.
# `RSpec.describe`는 커버되는 DevOps 단계입니다.
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
블록). - 문서 스타일 가이드는 간결하게 쓰는 방법과 능동태로 작성하는 데 대한 권장사항을 제공합니다.
- 긴 설명이나 여러 조건부는 분할되어야 할 지적 일 수 있습니다 (추가
- 가장 바깥쪽
Rspec.describe
블록은 DevOps 단계 이름이어야 합니다. -
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
대신 before(:all)
블록에 인스턴스 변수를 사용하십시오.
변수가 여러 예제에서 공유될 수 없는 경우에는 let
을 사용하십시오.
before(:context)
및 after
후크에서 UI 사용 제한
before(:context)
후크를 사용하여 API 호출, 비 UI 작업 또는 로그인과 같은 기본적인 UI 작업을 수행하도록 제한하십시오.
capybara-screenshot
라이브러리를 사용하여 실패 시 자동으로 스크린샷을 저장합니다.
capybara-screenshot
은 RSpect의 after
후크에서 스크린샷을 저장합니다.
before(:context)
에서 실패하는 경우 after
후크가 호출되지 않기 때문에 스크린샷이 저장되지 않습니다. 이 사실을 고려하여 before(:context)
의 사용을 스크린샷이 필요하지 않은 작업에만 한정하십시오.
마찬가지로 after
후크는 비 UI 작업에만 사용되어야 합니다. 테스트 파일의 after
후크에 UI 작업이 포함되면 스크린샷을 촬영하는 after
후크보다 이전에 실행되어 실패 지점에서 UI 상태를 이동시켜 스크린샷을 제대로 캡처하지 못할 수 있습니다.
브라우저에 로그인된 상태를 유지하지 않도록 테스트 보장
모든 테스트는 테스트 시작 시 로그인할 수 있다고 예상합니다.
예시는 issue #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 요청을 생성합니다.
# 커밋 리소스 사용
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.commit_message = '초기 커밋'
commit.add_files([
{ file_path: 'README.md', content: '안녕하세요, GitLab' }
])
end
# ProjectPush 사용
Resource::Repository::ProjectPush.fabricate! do |push|
push.commit_message = '초기 커밋'
push.file_name = 'README.md'
push.file_content = '안녕하세요, 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: '이슈 추가').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
확인 속도 향상
그러나 때로는 어떤 것이 우리가 원하는대로 아니라고 확인하고 싶은 경우도 있습니다. 즉, 어떤 것이 없음을 확실히 하고자 합니다. 유닛 테스트와 피처 스펙에서는 일반적으로 not_to
를 사용합니다.
왜냐하면 RSpec의 내장 매처와 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
이 Merge Request에서 실제로 사용자 정의 매처를 추가하는 실제 예시를 확인하세요.
우리는 qa/spec/support/matchers
에서 사용자 정의 부정 가능한 매처를 생성합니다.
not_to
를 사용하는 경우에만 사용자 정의 부정 가능한 매처를 생성해야 하며, to have_no_*
를 사용하는 경우에는 부정 가능한 매처가 필요하지 않지만 코드 가독성을 높일 수 있습니다.네게이터블 매처가 필요한 이유
다음 코드를 고려해보지만 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'
이 존재하고 있고, 우리가 그것이 사라질 때까지 기다리는 경우, 이 문은 실패할 것입니다.
사용자 정의 네게이터블 매처를 만들면 어떠한 문제도 발생하지 않습니다. 왜냐하면 has_no_job?
예측 메서드가 사용되기 때문에 작업이 사라질 때까지 필요한 시간만큼만 기다립니다.
마지막으로, 네게이터블 매처는 have_no_*
형태의 매처를 사용하는 것보다 선호됩니다. 왜냐하면 not_to
를 사용하여 매처를 부정하는 것이 일반적이고 익숙한 연습이기 때문입니다. 우리가 네게이터블 매처를 추가하여 그 연습을 용이하게 한다면, 후속 테스트 저자들이 효율적인 테스트를 작성하기 쉬워집니다.
puts
대신에 로거를 사용하세요
우리는 현재 GitLab QA 응용 프로그램과 종단 간 테스트에서 로그를 처리하기 위해 Rails logger
를 사용합니다.
이는 puts
와 비교하여 추가적인 기능을 제공합니다:
- 로깅 레벨을 지정하는 기능.
- 비슷한 로그에 태그를 지정하는 기능.
- 자동으로 서식을 맞추는 로그 메시지.