엔드 투 엔드 테스트 모범 사례

이는 테스트 가이드에서 발견된 모범 사례의 맞춤형 확장입니다.

클래스 및 모듈 명명

QA 프레임워크는 클래스 및 모듈 자동 로딩을 위해 Zeitwerk를 사용합니다. 기본 Zeitwerk inflector는 snake_cased 파일 이름을 PascalCased 모듈 또는 클래스 이름으로 변환합니다. 매뉴얼로 변형을 관리하지 않기 위해 이 패턴을 따르는 것이 좋습니다.

사용자 정의 변형 논리가 필요한 경우, 사용자 정의 inflectors는 loader.inflector.inflect 메서드 호출에서 qa.rb 파일에 추가됩니다.

테스트를 테스트 케이스에 연결

모든 테스트는 GitLab 프로젝트 테스트 케이스에서 해당하는 테스트 케이스와 Quality Test Cases 프로젝트에서 결과 이슈를 가져야 합니다.

테스트 케이스 이슈가 아직 존재하지 않는 경우, 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 '선택되지 않은 유지 관리가' do |testcase|
  it '사용자가 푸시를 실패함', testcase: testcase do
    ...
  end
end

RSpec.shared_examples '선택된 개발자' do |testcase|
  it '사용자가 푸시하고 병합함', testcase: testcase do
    ...
  end
end

다음과 같이 공유 예제를 포함하는 테스트를 고려해보세요:

RSpec.describe 'Create' do
  describe '제한된 보호 브랜치 푸시 및 병합' do
    context '단 하나의 사용자가 보호 브랜치에 병합하고 푸시할 수 있는 경우' do
      ...

      it_behaves_like '선택되지 않은 유지 관리가', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347775'
      it_behaves_like '선택된 개발자', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347774'
    end

    context '단 하나의 그룹이 보호 브랜치에 병합하고 푸시할 수 있는 경우' do
      ...

      it_behaves_like '선택되지 않은 유지 관리가', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347772'
      it_behaves_like '선택된 개발자', '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
  1. 모든 describe, context, 및 it 블록은 짧은 설명이 첨부되어야 합니다.

  2. 설명은 가능한 한 간결하게 유지해야 합니다.
    1. 긴 설명이나 여러 조건이 포함되어 있으면 추가 context 블록으로 분리해야 할 신호일 수 있습니다.
    2. 문서 스타일 가이드는 간결하게 쓰고 능동태로 작성하는 데 대한 권장 사항을 제공합니다.
  3. 가장 바깥쪽의 Rspec.describe 블록은 DevOps 단계 이름이어야 합니다.

  4. Rspec.describe 블록 내부에는 테스트할 기능의 이름을 가진 describe 블록이 있습니다.

  5. 선택적 context 블록은 테스트할 조건을 정의합니다.
    1. context 블록의 설명은 RuboCop 규칙에 맞춰 when, with, without, for, and, on, in, as, 또는 if로 시작해야 합니다.
  6. it 블록은 테스트의 통과/실패 기준을 설명합니다.
    1. 단일 예제의 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 '검색 결과 테스트' 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 '검색', :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 '검색' 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 "소유자 역할과 소유자 권한이 있습니다" 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-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?)

  # 로그인한 상태에서 작업 수행

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

관리자 접근이 필요한 테스트 태그 지정

관리자 접근이 필요한 테스트는 프로덕션 환경에서 실행하지 않습니다.

관리자 접근이 필요한 새 테스트를 추가할 때는 해당 테스트가 프로덕션 및 그렇게 테스트를 실행하고 싶지 않은 다른 환경에서 제외되도록 RSpec 메타데이터 :requires_admin를 적용하세요.

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

노트:

만약 관리자 접근이 필요한 테스트에서 유일한 작업이 기능 플래그를 전환하는 것이라면 대신 feature_flag 태그를 사용하세요. 기능 플래그를 사용하는 테스트에 대한 더 많은 정보는 기능 플래그로 테스트하기에서 확인할 수 있습니다.

Commit 리소스를 ProjectPush 보다 선호하세요

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

사용자 정의 매처 추가의 실제 예제에 대한 이 머지 요청을 참조하세요.

우리는 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'이 나타날 때까지 최대 10초를 기다리며 false를 반환한다는 점입니다.

예상되는 조건 하에서는 이 테스트가 필요 이상으로 10초가 더 걸릴 것입니다.

대신, 대기 시간을 0으로 강제로 설정할 수 있습니다:

# 나쁘지 않지만 잠재적으로 불안정
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 대신 logger 사용하기

현재 우리는 GitLab QA 애플리케이션과 엔드 투 엔드 테스트에서 로그 처리를 위해 Rails logger를 사용하고 있습니다.

이는 puts에 비해 다음과 같은 추가 기능을 제공합니다:

  • 로그 수준을 지정할 수 있는 기능.
  • 유사한 로그를 태그할 수 있는 기능.
  • 로그 메시지를 자동으로 형식 지정하는 기능.