InfoGrab DocsInfoGrab Docs

End-to-end 테스트 모범 사례

요약

이 문서는 테스팅 가이드에 있는 모범 사례를 End-to-end 테스트에 맞게 확장한 내용입니다. QA 프레임워크는 클래스 및 모듈 자동 로딩을 위해 Zeitwerk를 사용합니다. 커스텀 inflection 로직이 필요한 경우, qa.rb 파일의 loader.inflector.inflect 메서드 호출에 커스텀 inflector를 추가합니다.

이 문서는 테스팅 가이드에 있는 모범 사례를 End-to-end 테스트에 맞게 확장한 내용입니다.

클래스 및 모듈 네이밍#

QA 프레임워크는 클래스 및 모듈 자동 로딩을 위해 Zeitwerk를 사용합니다. 기본 Zeitwerk inflector는 snake_case 파일명을 PascalCase 모듈 또는 클래스명으로 변환합니다. inflection 수동 관리를 피하기 위해 이 패턴을 따르는 것이 권장됩니다.

커스텀 inflection 로직이 필요한 경우, qa.rb 파일의 loader.inflector.inflect 메서드 호출에 커스텀 inflector를 추가합니다.

테스트와 테스트 케이스 연결#

모든 테스트는 GitLab 프로젝트 테스트 케이스에 대응하는 테스트 케이스와 Quality Test Cases 프로젝트의 결과 이슈를 가져야 합니다. 테스트 케이스 이슈가 아직 존재하지 않는 경우, 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` is the DevOps Stage being covered
RSpec.describe 'Plan', feature_category: :wiki do
  # `describe` is the feature being tested
  describe 'wiki content creation' do
    # `context` provides the condition being covered
    context 'in a project'
      # `it` defines the expected result of the test
      it 'adds a home page'
      ...
      end
    ...
    end
  ...
  end
end
  • 모든 describe, context, it 블록에는 짧은 설명이 첨부되어야 합니다.

  • 설명은 가능한 한 간결하게 유지하세요.

설명이 길거나 조건이 여러 개인 경우, 분리해야 한다는 신호일 수 있습니다 (추가 context 블록 활용).

  • 문서 스타일 가이드는 간결하게 작성하는 방법과 능동태 사용법에 대한 권장 사항을 제공합니다.

  • 가장 바깥쪽 Rspec.describe 블록은 DevOps Stage 이름이어야 합니다.

  • Rspec.describe 블록 내부에는 테스트 중인 기능 이름을 가진 describe 블록이 있습니다.

  • 선택적 context 블록은 테스트 중인 조건을 정의합니다.

context 블록 설명은 RuboCop 규칙에 맞추어 when, with, without, for, and, on, in, as, 또는 if로 시작해야 합니다.

  • it 블록은 테스트의 통과/실패 기준을 설명합니다.

단일 예시가 있는 shared_examples에서는 명명된 it 블록 대신 specify 블록을 사용할 수 있습니다.

UI보다 API를 선호합니다#

End-to-end 테스트 프레임워크는 케이스별로 리소스를 생성하는 기능을 갖추고 있습니다. 가능하면 리소스는 API를 통해 생성해야 합니다.

API를 통해 테스트에 필요한 리소스를 생성하면 시간과 비용을 절감할 수 있습니다.

리소스에 대해 더 알아보기.

불필요한 기대값 피하기#

테스트를 간결하게 유지하려면 테스트에 필요한 것만 테스트하는 것이 중요합니다.

테스트해야 하는 내용과 관련 없는 expect() 구문을 추가하지 마세요.

예시:

#=> Good
Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
  expect(menu).to be_signed_in
end

#=> Bad
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) #=>  we already validated being signed in. redundant.
  expect(menu).to have_element(:nav_bar) #=> likely unnecessary. already validated in lower-level. test doesn't call for validating this.
end

#=> Good
issue = create(:issue, name: 'issue-name')

Project::Issues::Index.perform do |index|
  expect(index).to have_issue(issue)
end

#=> Bad
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) #=> page content check is redundant as the issue was already validated in the line above.
end

연속 기대값이 있을 때 aggregate_failures를 선호합니다#

여러 기대값이 있을 때 aggregate failures를 선호합니다를 참고하세요.

여러 기대값이 있을 때 aggregate_failures를 선호합니다#

테스트 케이스 내에 여러 기대값이 있어야 하는 경우 aggregate_failures를 사용하는 것이 바람직합니다.

이를 통해 기대값 집합을 그룹화하여 첫 번째 실패 시 테스트가 중단되는 대신 모든 실패를 한꺼번에 확인할 수 있습니다.

예시:

#=> Good
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

#=> Bad
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 메타데이터를 첨부합니다.

#=> Good
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

#=> Bad
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와 같이 테스트의 private 메서드에 일부 액션과 기대값을 캡슐화하는 경우:

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

그런 다음, 메서드 자체에서:

#=> Good
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

#=> Bad
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

여러 파일에 테스트 분리하기를 선호합니다#

저희 프레임워크는 spec 파일을 병렬로 실행하는 몇 가지 병렬화 메커니즘을 포함하고 있습니다.

그러나 테스트는 테스트/예시 단위가 아닌 spec 파일 단위로 병렬화되므로, 기존 파일에 새 테스트를 추가하면 더 높은 병렬화를 달성할 수 없습니다.

그렇더라도 기존 파일에 새 테스트를 추가할 다른 이유가 있을 수 있습니다.

예를 들어, 테스트가 설정 비용이 높은 상태를 공유하는 경우, 해당 설정을 사용하는 테스트를 병렬화할 수 없더라도 설정을 한 번만 수행하는 것이 더 효율적일 수 있습니다.

요약:

  • 권장: 테스트가 비용이 많이 드는 설정을 공유하지 않는 한 별도의 파일에 테스트를 분리하세요.

  • 비권장: 병렬화에 미치는 영향을 고려하지 않고 기존 파일에 새 테스트를 추가하지 마세요.

let 변수 대 인스턴스 변수#

기본적으로 let 또는 인스턴스 변수를 사용할 때 테스팅 모범 사례를 따르세요. 그러나 End-to-end 테스트에서는 리소스 생성과 같은 설정 비용이 높습니다. 리소스를 저장하는 데 let을 사용하면 각 예시마다 별도로 리소스가 생성됩니다. 리소스를 여러 예시 간에 공유할 수 있는 경우, 실행 시간을 절약하기 위해 let 대신 before(:all) 블록의 인스턴스 변수를 사용하세요. 변수를 여러 예시가 공유할 수 없는 경우 let을 사용하세요.

before(:context) 및 after 훅에서 UI 사용 제한#

before(:context) 훅은 API 호출, 비 UI 작업, 또는 로그인과 같은 기본 UI 작업만으로 설정 작업을 수행하도록 제한하세요.

저희는 실패 시 스크린샷을 자동으로 저장하기 위해 capybara-screenshot 라이브러리를 사용합니다.

capybara-screenshotRSpec의 after 훅에서 스크린샷을 저장합니다. before(:context)에서 실패가 발생하면 after 훅이 호출되지 않으므로 스크린샷이 저장되지 않습니다.

이 사실을 고려하여 before(:context) 사용을 스크린샷이 필요하지 않은 작업으로만 제한해야 합니다.

마찬가지로, after 훅은 비 UI 작업에만 사용해야 합니다. 테스트 파일의 after 훅에서 UI 작업이 있으면 스크린샷을 찍는 after 훅보다 먼저 실행됩니다. 이로 인해 UI 상태가 실패 지점에서 벗어나게 되어 스크린샷이 올바른 시점에 캡처되지 않습니다.

테스트가 브라우저 로그인 상태로 남지 않도록 보장하기#

모든 테스트는 테스트 시작 시 로그인할 수 있다고 가정합니다.

예시는 이슈 #34736을 참고하세요.

이상적으로는 after(:context) (또는 before(:context)) 블록에서 수행하는 작업은 API를 사용해서 수행합니다. 사용자 인터페이스로 수행해야 하는 경우(예: API 기능이 없는 경우), 블록 끝에서 반드시 로그아웃하세요.

after(:all) do
  login unless Page::Main::Menu.perform(&:signed_in?)

  # Do something while logged in

  Page::Main::Menu.perform(&:sign_out)
end

관리자 액세스가 필요한 테스트에 태그 달기#

저희는 프로덕션 환경에서 관리자 액세스가 필요한 테스트를 실행하지 않습니다.

관리자 액세스가 필요한 새 테스트를 추가할 때, 프로덕션 및 해당 테스트를 실행하지 않으려는 기타 환경에서 실행하는 테스트 스위트에 포함되지 않도록 RSpec 메타데이터 :requires_admin을 적용하세요.

로컬에서 테스트를 실행하거나 파이프라인을 구성할 때 환경 변수 QA_CAN_TEST_ADMIN_FEATURESfalse로 설정하여 :requires_admin 태그가 있는 테스트를 건너뛸 수 있습니다.

테스트에서 관리자 액세스가 필요한 유일한 액션이 기능 플래그를 전환하는 것이라면 :requires_admin 대신 feature_flag 태그를 사용하세요. 자세한 내용은 기능 플래그를 사용한 테스팅에서 확인하세요.

ProjectPush보다 Commit 리소스를 선호합니다#

API 사용에 맞게, 가능하면 Commit 리소스를 사용하세요.

ProjectPush는 Git 명령줄 인터페이스(CLI)의 원시 셸 명령을 사용하고, Commit 리소스는 HTTP 요청을 만듭니다.

# Using a commit resource
Resource::Repository::Commit.fabricate_via_api! do |commit|
  commit.commit_message = 'Initial commit'
  commit.add_files([
    { file_path: 'README.md', content: 'Hello, GitLab' }
  ])
end

# Using a 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를 클릭하는 것은 피하세요. 이 액션은 의도치 않게 다른 요소를 클릭하여 테스트 상태를 변경하고 실패를 유발할 수 있습니다.

# Clicking another element to blur an input
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)
  # Clicking the title blurs the input
  click_element(:title)
  click_element(:add_issue_button)
end

# Using native mouse click events in the case of a mask/overlay
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 확인 속도를 높이기 위한 부정 가능 매처 생성하기#

그러나 때로는 원하지 않는 상태가 아닌지 확인하고 싶을 때가 있습니다. 즉, 무언가가 없는지 확인하고 싶습니다. 단위 테스트 및 기능 스펙의 경우, RSpec의 내장 매처와 Capybara의 매처 모두 부정 가능하기 때문에 일반적으로 not_to를 사용하며, 이는 다음 두 구문이 동일함을 의미합니다.

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를 사용하는 경우에만 생성해야 합니다. to have_no_*를 사용하는 경우 부정 가능 매처가 필요하지 않지만 코드 가독성이 향상됩니다.

부정 가능 매처가 필요한 이유#

다음 코드를 살펴보세요. 단, have_job에 대한 커스텀 부정 가능 매처가 없다고 가정합니다.

# Bad
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')false를 반환하기 전에 최대 10초 동안 'a job'이 나타나기를 기다린다는 것입니다. 예상 조건에서 이 테스트는 필요 이상으로 10초가 더 걸립니다.

대신 대기를 강제로 없애는 방법을 사용할 수 있습니다:

# Not as bad but potentially flaky
Page::Project::Pipeline::Show.perform do |pipeline|
  expect(pipeline).not_to have_job('a_job', wait: 0)
end

문제는 'a_job'이 존재하고 사라지기를 기다리는 경우 이 구문이 실패한다는 것입니다.

커스텀 부정 가능 매처를 생성하면 두 가지 문제 모두 발생하지 않습니다. has_no_job? 술어 메서드가 사용되어 job이 사라지는 데 필요한 시간만큼만 기다리기 때문입니다.

마지막으로, 부정 가능 매처는 not_to를 사용하여 매처를 부정하는 것이 일반적이고 친숙한 관행이기 때문에 have_no_* 형태의 매처보다 선호됩니다. 부정 가능 매처를 추가하여 이 관행을 지원하면 이후 테스트 작성자가 효율적인 테스트를 작성하기 쉬워집니다.

puts보다 logger를 사용하세요#

저희는 현재 GitLab QA 애플리케이션과 End-to-end 테스트 모두에서 로그를 처리하기 위해 Rails logger를 사용합니다. 이는 puts에 비해 다음과 같은 추가 기능을 제공합니다:

  • 로깅 레벨 지정 기능.

  • 유사한 로그 태그 지정 기능.

  • 로그 메시지 자동 포맷 기능.

End-to-end 테스트 모범 사례

GitLab v19.1
원문 보기
요약

이 문서는 테스팅 가이드에 있는 모범 사례를 End-to-end 테스트에 맞게 확장한 내용입니다. QA 프레임워크는 클래스 및 모듈 자동 로딩을 위해 Zeitwerk를 사용합니다. 커스텀 inflection 로직이 필요한 경우, qa.rb 파일의 loader.inflector.inflect 메서드 호출에 커스텀 inflector를 추가합니다.

이 문서는 테스팅 가이드에 있는 모범 사례를 End-to-end 테스트에 맞게 확장한 내용입니다.

클래스 및 모듈 네이밍#

QA 프레임워크는 클래스 및 모듈 자동 로딩을 위해 Zeitwerk를 사용합니다. 기본 Zeitwerk inflector는 snake_case 파일명을 PascalCase 모듈 또는 클래스명으로 변환합니다. inflection 수동 관리를 피하기 위해 이 패턴을 따르는 것이 권장됩니다.

커스텀 inflection 로직이 필요한 경우, qa.rb 파일의 loader.inflector.inflect 메서드 호출에 커스텀 inflector를 추가합니다.

테스트와 테스트 케이스 연결#

모든 테스트는 GitLab 프로젝트 테스트 케이스에 대응하는 테스트 케이스와 Quality Test Cases 프로젝트의 결과 이슈를 가져야 합니다. 테스트 케이스 이슈가 아직 존재하지 않는 경우, 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` is the DevOps Stage being covered
RSpec.describe 'Plan', feature_category: :wiki do
  # `describe` is the feature being tested
  describe 'wiki content creation' do
    # `context` provides the condition being covered
    context 'in a project'
      # `it` defines the expected result of the test
      it 'adds a home page'
      ...
      end
    ...
    end
  ...
  end
end
  • 모든 describe, context, it 블록에는 짧은 설명이 첨부되어야 합니다.

  • 설명은 가능한 한 간결하게 유지하세요.

설명이 길거나 조건이 여러 개인 경우, 분리해야 한다는 신호일 수 있습니다 (추가 context 블록 활용).

  • 문서 스타일 가이드는 간결하게 작성하는 방법과 능동태 사용법에 대한 권장 사항을 제공합니다.

  • 가장 바깥쪽 Rspec.describe 블록은 DevOps Stage 이름이어야 합니다.

  • Rspec.describe 블록 내부에는 테스트 중인 기능 이름을 가진 describe 블록이 있습니다.

  • 선택적 context 블록은 테스트 중인 조건을 정의합니다.

context 블록 설명은 RuboCop 규칙에 맞추어 when, with, without, for, and, on, in, as, 또는 if로 시작해야 합니다.

  • it 블록은 테스트의 통과/실패 기준을 설명합니다.

단일 예시가 있는 shared_examples에서는 명명된 it 블록 대신 specify 블록을 사용할 수 있습니다.

UI보다 API를 선호합니다#

End-to-end 테스트 프레임워크는 케이스별로 리소스를 생성하는 기능을 갖추고 있습니다. 가능하면 리소스는 API를 통해 생성해야 합니다.

API를 통해 테스트에 필요한 리소스를 생성하면 시간과 비용을 절감할 수 있습니다.

리소스에 대해 더 알아보기.

불필요한 기대값 피하기#

테스트를 간결하게 유지하려면 테스트에 필요한 것만 테스트하는 것이 중요합니다.

테스트해야 하는 내용과 관련 없는 expect() 구문을 추가하지 마세요.

예시:

#=> Good
Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
  expect(menu).to be_signed_in
end

#=> Bad
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) #=>  we already validated being signed in. redundant.
  expect(menu).to have_element(:nav_bar) #=> likely unnecessary. already validated in lower-level. test doesn't call for validating this.
end

#=> Good
issue = create(:issue, name: 'issue-name')

Project::Issues::Index.perform do |index|
  expect(index).to have_issue(issue)
end

#=> Bad
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) #=> page content check is redundant as the issue was already validated in the line above.
end

연속 기대값이 있을 때 aggregate_failures를 선호합니다#

여러 기대값이 있을 때 aggregate failures를 선호합니다를 참고하세요.

여러 기대값이 있을 때 aggregate_failures를 선호합니다#

테스트 케이스 내에 여러 기대값이 있어야 하는 경우 aggregate_failures를 사용하는 것이 바람직합니다.

이를 통해 기대값 집합을 그룹화하여 첫 번째 실패 시 테스트가 중단되는 대신 모든 실패를 한꺼번에 확인할 수 있습니다.

예시:

#=> Good
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

#=> Bad
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 메타데이터를 첨부합니다.

#=> Good
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

#=> Bad
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와 같이 테스트의 private 메서드에 일부 액션과 기대값을 캡슐화하는 경우:

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

그런 다음, 메서드 자체에서:

#=> Good
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

#=> Bad
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

여러 파일에 테스트 분리하기를 선호합니다#

저희 프레임워크는 spec 파일을 병렬로 실행하는 몇 가지 병렬화 메커니즘을 포함하고 있습니다.

그러나 테스트는 테스트/예시 단위가 아닌 spec 파일 단위로 병렬화되므로, 기존 파일에 새 테스트를 추가하면 더 높은 병렬화를 달성할 수 없습니다.

그렇더라도 기존 파일에 새 테스트를 추가할 다른 이유가 있을 수 있습니다.

예를 들어, 테스트가 설정 비용이 높은 상태를 공유하는 경우, 해당 설정을 사용하는 테스트를 병렬화할 수 없더라도 설정을 한 번만 수행하는 것이 더 효율적일 수 있습니다.

요약:

  • 권장: 테스트가 비용이 많이 드는 설정을 공유하지 않는 한 별도의 파일에 테스트를 분리하세요.

  • 비권장: 병렬화에 미치는 영향을 고려하지 않고 기존 파일에 새 테스트를 추가하지 마세요.

let 변수 대 인스턴스 변수#

기본적으로 let 또는 인스턴스 변수를 사용할 때 테스팅 모범 사례를 따르세요. 그러나 End-to-end 테스트에서는 리소스 생성과 같은 설정 비용이 높습니다. 리소스를 저장하는 데 let을 사용하면 각 예시마다 별도로 리소스가 생성됩니다. 리소스를 여러 예시 간에 공유할 수 있는 경우, 실행 시간을 절약하기 위해 let 대신 before(:all) 블록의 인스턴스 변수를 사용하세요. 변수를 여러 예시가 공유할 수 없는 경우 let을 사용하세요.

before(:context) 및 after 훅에서 UI 사용 제한#

before(:context) 훅은 API 호출, 비 UI 작업, 또는 로그인과 같은 기본 UI 작업만으로 설정 작업을 수행하도록 제한하세요.

저희는 실패 시 스크린샷을 자동으로 저장하기 위해 capybara-screenshot 라이브러리를 사용합니다.

capybara-screenshotRSpec의 after 훅에서 스크린샷을 저장합니다. before(:context)에서 실패가 발생하면 after 훅이 호출되지 않으므로 스크린샷이 저장되지 않습니다.

이 사실을 고려하여 before(:context) 사용을 스크린샷이 필요하지 않은 작업으로만 제한해야 합니다.

마찬가지로, after 훅은 비 UI 작업에만 사용해야 합니다. 테스트 파일의 after 훅에서 UI 작업이 있으면 스크린샷을 찍는 after 훅보다 먼저 실행됩니다. 이로 인해 UI 상태가 실패 지점에서 벗어나게 되어 스크린샷이 올바른 시점에 캡처되지 않습니다.

테스트가 브라우저 로그인 상태로 남지 않도록 보장하기#

모든 테스트는 테스트 시작 시 로그인할 수 있다고 가정합니다.

예시는 이슈 #34736을 참고하세요.

이상적으로는 after(:context) (또는 before(:context)) 블록에서 수행하는 작업은 API를 사용해서 수행합니다. 사용자 인터페이스로 수행해야 하는 경우(예: API 기능이 없는 경우), 블록 끝에서 반드시 로그아웃하세요.

after(:all) do
  login unless Page::Main::Menu.perform(&:signed_in?)

  # Do something while logged in

  Page::Main::Menu.perform(&:sign_out)
end

관리자 액세스가 필요한 테스트에 태그 달기#

저희는 프로덕션 환경에서 관리자 액세스가 필요한 테스트를 실행하지 않습니다.

관리자 액세스가 필요한 새 테스트를 추가할 때, 프로덕션 및 해당 테스트를 실행하지 않으려는 기타 환경에서 실행하는 테스트 스위트에 포함되지 않도록 RSpec 메타데이터 :requires_admin을 적용하세요.

로컬에서 테스트를 실행하거나 파이프라인을 구성할 때 환경 변수 QA_CAN_TEST_ADMIN_FEATURESfalse로 설정하여 :requires_admin 태그가 있는 테스트를 건너뛸 수 있습니다.

테스트에서 관리자 액세스가 필요한 유일한 액션이 기능 플래그를 전환하는 것이라면 :requires_admin 대신 feature_flag 태그를 사용하세요. 자세한 내용은 기능 플래그를 사용한 테스팅에서 확인하세요.

ProjectPush보다 Commit 리소스를 선호합니다#

API 사용에 맞게, 가능하면 Commit 리소스를 사용하세요.

ProjectPush는 Git 명령줄 인터페이스(CLI)의 원시 셸 명령을 사용하고, Commit 리소스는 HTTP 요청을 만듭니다.

# Using a commit resource
Resource::Repository::Commit.fabricate_via_api! do |commit|
  commit.commit_message = 'Initial commit'
  commit.add_files([
    { file_path: 'README.md', content: 'Hello, GitLab' }
  ])
end

# Using a 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를 클릭하는 것은 피하세요. 이 액션은 의도치 않게 다른 요소를 클릭하여 테스트 상태를 변경하고 실패를 유발할 수 있습니다.

# Clicking another element to blur an input
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)
  # Clicking the title blurs the input
  click_element(:title)
  click_element(:add_issue_button)
end

# Using native mouse click events in the case of a mask/overlay
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 확인 속도를 높이기 위한 부정 가능 매처 생성하기#

그러나 때로는 원하지 않는 상태가 아닌지 확인하고 싶을 때가 있습니다. 즉, 무언가가 없는지 확인하고 싶습니다. 단위 테스트 및 기능 스펙의 경우, RSpec의 내장 매처와 Capybara의 매처 모두 부정 가능하기 때문에 일반적으로 not_to를 사용하며, 이는 다음 두 구문이 동일함을 의미합니다.

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를 사용하는 경우에만 생성해야 합니다. to have_no_*를 사용하는 경우 부정 가능 매처가 필요하지 않지만 코드 가독성이 향상됩니다.

부정 가능 매처가 필요한 이유#

다음 코드를 살펴보세요. 단, have_job에 대한 커스텀 부정 가능 매처가 없다고 가정합니다.

# Bad
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')false를 반환하기 전에 최대 10초 동안 'a job'이 나타나기를 기다린다는 것입니다. 예상 조건에서 이 테스트는 필요 이상으로 10초가 더 걸립니다.

대신 대기를 강제로 없애는 방법을 사용할 수 있습니다:

# Not as bad but potentially flaky
Page::Project::Pipeline::Show.perform do |pipeline|
  expect(pipeline).not_to have_job('a_job', wait: 0)
end

문제는 'a_job'이 존재하고 사라지기를 기다리는 경우 이 구문이 실패한다는 것입니다.

커스텀 부정 가능 매처를 생성하면 두 가지 문제 모두 발생하지 않습니다. has_no_job? 술어 메서드가 사용되어 job이 사라지는 데 필요한 시간만큼만 기다리기 때문입니다.

마지막으로, 부정 가능 매처는 not_to를 사용하여 매처를 부정하는 것이 일반적이고 친숙한 관행이기 때문에 have_no_* 형태의 매처보다 선호됩니다. 부정 가능 매처를 추가하여 이 관행을 지원하면 이후 테스트 작성자가 효율적인 테스트를 작성하기 쉬워집니다.

puts보다 logger를 사용하세요#

저희는 현재 GitLab QA 애플리케이션과 End-to-end 테스트 모두에서 로그를 처리하기 위해 Rails logger를 사용합니다. 이는 puts에 비해 다음과 같은 추가 기능을 제공합니다:

  • 로깅 레벨 지정 기능.

  • 유사한 로그 태그 지정 기능.

  • 로그 메시지 자동 포맷 기능.