엔드 투 엔드 테스트 베스트 프랙티스

이것은 테스팅 가이드에서 찾을 수 있는 베스트 프랙티스를 개정한 것입니다.

클래스 및 모듈 네이밍

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
  1. describe, context, it 블록은 짧은 설명이 함께해야합니다.
  2. 설명을 가능한 한 간결하게 유지해야 합니다.
    1. 긴 설명이나 여러 조건문은 분할되어야 할 지표일 수 있습니다(추가 context 블록).
    2. Documentation Style Guide는 간결하게 작성하고 능동태로 작성하는 방법에 대한 권장사항을 제공합니다.
  3. 가장 바깥쪽 Rspec.describe 블록은 DevOps stage 이름이어야 합니다.
  4. Rspec.describe 블록 내부에는 테스트되는 기능의 이름이 있는 describe 블록이 있습니다.
  5. 선택적 context 블록은 테스트되는 조건을 정의합니다.
    1. 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-screenshotRSpec의 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_FEATURESfalse로 설정하여 :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와 비교했을 때 추가 기능을 제공합니다. 예를 들어:

  • 로깅 레벨을 지정할 수 있는 기능.
  • 유사한 로그에 태그를 지정할 수 있는 기능.
  • 로그 메시지 자동 형식화 기능.