- 클래스 및 모듈 네이밍
- 테스트를 해당 테스트 케이스에 연결
- 테스트 네이밍
- UI 대신 API 선호
- 여분의 기대는 피하십시오
- 연이은 기대가 있는 경우
aggregate_failures
를 선호 - 여러 기대사항이 있는 경우
aggregate_failures
를 선호 expect do ... raise_error
블록에서 여러 동작을 피하세요- 테스트를 여러 파일로 분할하는 것을 선호하세요
let
변수 vs 인스턴스 변수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 > 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’로 시작해야 합니다.
-
-
it
블록은 테스트의 통과/실패 기준을 설명합니다.- 단일 예제로 공유된
shared_examples
인 경우 명명된it
블록 대신specify
블록을 사용할 수 있습니다.
- 단일 예제로 공유된
UI 대신 API 선호
엔드투엔드 테스트 프레임워크는 경우에 따라 자원을 만들 수 있는 능력을 갖추고 있습니다. 가능한 경우 리소스는 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
를 선호에서 확인하세요.
여러 기대사항이 있는 경우 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
변수 vs 인스턴스 변수
기본적으로 let
이나 인스턴스 변수를 사용할 때 테스트 최적의 방법을 따르세요. 그러나 엔드 투 엔드 테스트의 경우, 리소스 생성과 같은 설정은 비용이 많이 드는 경우가 많습니다.
만약 리소스를 공유할 수 있다면, 각 예제마다 생성될 것을 고려하여 let
대신 before(:all)
블록에서 인스턴스 변수를 사용하여 실행 시간을 절약하세요.
변수가 여러 예제에서 공유될 수 없는 경우 let
을 사용하세요.
before(:context)
와 after
후크에서 UI 사용 제한
before(:context)
후크의 사용을 API 호출만, 비 UI 동작 또는 로그인과 같은 기본적인 UI 동작을 수행하는 것으로 제한하세요.
우리는 capybara-screenshot
라이브러리를 사용하여 실패 시 화면 샷을 자동으로 저장합니다.
capybara-screenshot
은 RSpect의 after
후크에 화면 샷을 저장합니다.
만약 before(:context)
에서 실패하는 경우, after
후크가 호출되지 않습니다 그래서 화면 샷이 저장되지 않습니다.
이와 유사하게, after
후크는 비 UI 동작에만 사용해야 합니다. 테스트 파일의 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
을 적용하여 해당 테스트가 프로덕션 및 기타 환경에서 실행되지 않도록 테스트 스위트에 포함되지 않도록 합니다.
로컬에서 테스트를 실행하거나 파이프라인을 설정할 때, :requires_admin
태그가 있는 테스트를 건너뛰기 위해 환경 변수 QA_CAN_TEST_ADMIN_FEATURES
를 false
로 설정할 수 있습니다.
참고:
관리자 액세스가 필요한 테스트의 유일한 작업이 기능 플래그를 토글하는 것인 경우, 대신 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('숨겨진')
except(page).to have_no_text('숨겨진')
유감스럽게도, 이것은 우리가 테스트 프레임워크에 추가한 술어 메서드에 자동으로 적용되지 않습니다. 페이지 객체에 추가된 우리 자신의 술어 메서드에 대한 부정 가능한 매처를 만들어야 합니다. 자체 부정 가능한 매처를 만들기 위한 것입니다.
초기 예제는 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
를 사용하는 경우에만 생성합니다. to have_no_*
를 사용하는 경우에는 부정 가능한 매처가 필요하지 않지만 코드 가독성을 높일 수 있습니다.
네거터블(matchers) 매쳐가 필요한 이유
다음 코드를 고려해보지만, have_job
에 대한 사용자 정의된 네거터블(matcher)이 없다고 가정해 봅시다.
# 안 좋은 예
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
를 반환하기 때문에 이 테스트는 예상 조건에서 필요 이상으로 10초 더 오래 걸립니다.
그 대신에, 우리는 기다리지 않도록 강제할 수 있습니다.
# 안 좋지 않지만 잠재적으로 부정확함
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job', wait: 0)
end
문제는, 만약 'a_job'
이 존재하고 우리가 그것이 사라질 때까지 기다리고 있다면, 이 문은 실패합니다.
사용자 정의된 네거터블(matcher)를 만든다면 어느 문제도 존재하지 않습니다. 왜냐하면 has_no_job?
예측 메서드가 사용되어서 작업이 사라질 때까지만 필요한 시간만큼만 기다리게 될 것이기 때문입니다.
마지막으로, 네거터블(matcher)은 have_no_*
형태의 matchers를 사용하는 것보다 선호됩니다. 왜냐하면 not_to
를 사용하여 matcher를 부정하는 것은 흔하고 익숙한 관행이기 때문입니다. 네거터블(matcher)를 추가함으로써 해당 관행을 용이하게 해주면, 후속 테스트 작성자가 효율적인 테스트를 쉽게 작성할 수 있도록 해줍니다.
puts
대신에 로거(logger) 사용하기
우리는 현재 GitLab QA 애플리케이션과 종단 간 테스트 모두에서 Rails logger
를 사용하여 로그를 처리합니다.
이는 puts
와 비교했을 때 다음과 같은 추가적인 기능을 제공합니다:
- 로깅 레벨을 지정할 수 있는 기능.
- 유사한 로그에 태그를 지정할 수 있는 기능.
- 자동으로 로그 메시지를 형식화하는 기능.