GitLab QA의 페이지 객체

GitLab QA에서는 _Page Objects_라는 알려진 패턴을 사용하고 있습니다.

이것은 GitLab의 모든 페이지에 대한 추상화를 구축하여 GitLab QA 시나리오를 진행할 때 사용한다는 것을 의미합니다. 우리가 페이지에서 무언가를 할 때, 양식을 작성하거나 버튼을 선택하는 등의 작업은 GitLab의 이 영역과 연결된 페이지 객체를 통해서만 수행합니다.

예를 들어, GitLab QA 테스트 하니스가 GitLab에 로그인할 때, 사용자 로그인 및 사용자 비밀번호를 입력해야 합니다. 이를 위해 Page::Main::Login라는 클래스와 sign_in_using_credentials 메소드가 있으며, 이는 user-loginuser-password 필드를 읽는 코드입니다.

왜 이게 필요할까요?

우리는 페이지 객체가 필요합니다. 이는 중복을 줄이고 GitLab 소스 코드의 선택자를 변경할 때 발생할 수 있는 문제를 피하기 위함입니다.

GitLab QA에 백 개의 스펙이 있고, 매번 GitLab에 로그인해야 한다고 상상해 보세요. 페이지 객체 없이, 사람들은 변동성이 큰 헬퍼에 의존하거나 Capybara 메소드를 직접 호출해야 합니다. 모든 *_spec.rb 파일 / 테스트 예제에서 fill_in 'user-login'을 호출하는 것을 상상해 보세요.

누군가 나중에 이 페이지와 관련된 뷰에서 t.text_field 'login't.text_field 'username'으로 변경한다면, 이는 다른 필드 식별자를 생성하여 모든 테스트를 깰 것입니다.

우리가 Page::Main::Login.perform(&:sign_in_using_credentials)를 언제나 사용하기 때문에, GitLab에 로그인할 때 페이지 객체는 단일 진실의 출처이며, 우리는 fill_in 'user-login'을 한 곳에서만 fill_in 'user-username'로 업데이트하면 됩니다.

우리는 과거에 어떤 문제를 겪었나요?

우리는 성능상의 이유로 모든 커밋에 대해 QA 테스트를 실행하지 않으며, 패키지를 빌드하고 모든 것을 테스트하는 데 필요한 시간 때문입니다.

그래서 누군가 새 세션 뷰에서 t.text_field 'login't.text_field 'username'으로 변경하면, 이 변경 사항을 GitLab QA 야간 파이프라인이 실패하거나 누군가가 자신의 머지 요청에서 package-and-qa 작업을 트리거할 때까지 알지 못합니다.

이러한 변경은 모든 테스트를 깨뜨립니다. 우리는 이 문제를 _취약한 테스트 문제_라고 부릅니다.

GitLab QA를 더 신뢰할 수 있고 견고하게 만들기 위해, 우리는 GitLab CE / EE 뷰와 GitLab QA 간의 결합을 도입하여 이 문제를 해결해야 했습니다.

취약한 테스트 문제를 어떻게 해결했나요?

현재, 새로운 Page::Base 파생 클래스를 추가할 때, 페이지 객체가 의존하는 모든 선택자를 정의해야 합니다.

코드를 CE / EE 리포지토리에 푸시할 때마다, CI 파이프라인의 일환으로 qa:selectors 적합성 테스트 작업이 실행됩니다.

이 테스트는 qa/page 디렉토리에 구현된 모든 페이지 객체를 검증합니다. 실패할 경우, 누락되었거나 잘못된 뷰/선택자 정의에 대해 알려줍니다.

페이지 객체를 올바르게 구현하는 방법은?

우리는 페이지 객체와 실제로 구현되는 GitLab 뷰 간의 결합을 정의하기 위한 DSL을 구축했습니다. 아래의 예를 참조하세요.

module Page
  module Main
    class Login < Page::Base
      view 'app/views/devise/passwords/edit.html.haml' do
        element 'password-field'
        element 'password-confirmation'
        element 'change-password-button'
      end

      view 'app/views/devise/sessions/_new_base.html.haml' do
        element 'login-field'
        element 'password-field'
        element 'sign-in-button'
      end

      # ...
    end
  end
end

요소 정의

view DSL 메서드는 요소를 렌더링하는 Rails 뷰, 부분 또는 Vue 컴포넌트에 해당합니다.

element DSL 메서드는 해당 요소에 대해 testid=element-name 데이터 속성을 뷰 파일에 추가해야 함을 선언합니다(아직 추가되지 않은 경우).

실제 뷰 코드와 일치하도록 값(문자열 또는 정규 표현식)을 정의할 수도 있지만, 이는 더 이상 사용되지 않으므로 위의 방법을 사용하는 것이 좋습니다. 이유는 다음과 같습니다:

  • 일관성: 요소를 정의하는 방법은 하나뿐입니다.
  • 관심사의 분리: 테스트는 다른 컴포넌트에서 사용되는 코드나 클래스(예: js-* 클래스 등)를 재사용하는 대신 전용 data-testid 속성을 사용합니다.
view 'app/views/my/view.html.haml' do

  ### 좋음 ###

  # 뷰에 `[data-testid="logout-button"]` CSS 선택자가 존재해야 함을 암시적으로 요구합니다.
  element 'logout-button'

  ### 나쁨 ###

  ## 이는 더 이상 사용되지 않으며 `QA/ElementWithPattern` RuboCop 규칙에 의해 금지됩니다.
  # `my/view.html.haml`에 `f.submit "Sign in"`이 존재해야 함을 요구합니다.
  element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern

  ## 이는 더 이상 사용되지 않으며 `QA/ElementWithPattern` RuboCop 규칙에 의해 금지됩니다.
  # `my/view.html.haml`의 모든 줄을
  # `/link_to .* "My Profile"/` 정규 표현식에 맞춥니다.
  element :profile_link, /link_to .* "My Profile"/ # rubocop:disable QA/ElementWithPattern
end

뷰에 요소 추가하기

다음 요소가 주어졌습니다…

view 'app/views/my/view.html.haml' do
  element 'login-field'
  element 'password-field'
  element 'sign-in-button'
end

이 요소를 뷰에 추가하려면 각 요소에 대해 data-testid 속성을 추가하여 Rails 뷰, 부분 또는 Vue 컴포넌트를 변경해야 합니다.

우리의 경우 data-testid="login-field", data-testid="password-field"data-testid="sign-in-button"이 됩니다.

app/views/my/view.html.haml

= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "이 필드는 필수입니다.", data: { testid: 'login_field' }
= f.password_field :password, class: "form-control bottom", required: true, title: "이 필드는 필수입니다.", data: { testid: 'password_field' }
= f.submit "Sign in", class: "btn btn-confirm", data: { testid: 'sign_in_button' }

기억할 점:

  • 요소의 이름과 data-testid는 일치해야 하며 케밥 케이스로 작성해야 합니다.
  • 요소가 페이지에 조건 없이 표시되는 경우 required: true를 요소에 추가해야 합니다. 동적 요소 검증을 참조하세요.
  • 페이지 객체에서 data-qa-selector 클래스가 보이지 않아야 합니다. 우리는 data-testid 정의 방법을 사용해야 합니다.

data-testid vs data-qa-selector

  • GitLab 16.1에서 소개됨

기존의 data-qa-selector 클래스는 더 이상 사용되지 않으므로 data-testid 정의 방법을 사용해야 합니다.

동적 요소 선택

자동화 테스트에서 일반적인 경우는 단일 “여러 개 중 하나” 요소를 선택하는 것입니다.

여러 항목의 목록에서 선택하는 것을 어떻게 구분합니까?

가장 일반적인 우회 방법은 텍스트 매칭을 통한 것입니다.

대신에, 더 나은 방법은 텍스트가 아닌 고유 식별자를 통해 해당 특정 요소와 일치시키는 것입니다.

우리는 data-qa-* 확장 가능한 선택 메커니즘을 추가하여 이 문제를 해결했습니다.

예시

예시 1

다음 Rails 뷰를 주어진다고 가정합니다 (GitLab Issues를 예로 사용):

%ul.issues-list
 - @issues.each do |issue|
   %li.issue{data: { testid: 'issue', qa_issue_title: issue.title } }= link_to issue

Rails 모델과 일치시켜 해당 특정 이슈를 선택할 수 있습니다.

class Page::Project::Issues::Index < Page::Base
  def has_issue?(issue)
    has_element?(:issue, issue_title: issue)
  end
end

테스트에서는 이 특정 이슈가 존재하는지 확인할 수 있습니다.

describe 'Issue' do
  it 'has an issue titled "hello"' do
    Page::Project::Issues::Index.perform do |index|
      expect(index).to have_issue('hello')
    end
  end
end

예시 2

인덱스를 통해…

%ol
  - @some_model.each_with_index do |model, idx|
    %li.model{ data: { testid: 'model', qa_index: idx } }
expect(the_page).to have_element(:model, index: 1) #=> 목록에 나타나는 첫 번째 모델을 선택

예외

일부 경우에는 선택기를 추가하는 것이 불가능하거나 가치가 없을 수 있습니다.

일부 UI 구성 요소는 외부 라이브러리를 사용하며, 이는 제3자가 유지 관리하는 것들도 포함됩니다.

GitLab에서 유지 관리되는 라이브러리에서도 선택기 유효성 검사기는 GitLab 프로젝트 내의 코드에서만 실행되므로 라이브러리에 있는 코드의 뷰 경로를 지정할 수 없습니다.

이러한 드문 경우에는 element를 추가할 수 없는 이유를 설명하는 주석과 함께 페이지 객체 메서드에서 CSS 선택기를 사용하는 것이 합리적입니다.

페이지 고려 사항 정의

일부 페이지는 공통 동작을 공유하고 있으며, EE 전용 메서드를 추가하는 EE 전용 모듈이 전에 붙어 있습니다.

이러한 모듈은 다음과 같아야 합니다:

  1. QA::Page::PageConcern 모듈에서 확장해야 하며, extend QA::Page::PageConcern을 사용해야 합니다.
  2. 다른 모듈을 포함하거나 추가해야 하는 경우 self.prepended 메서드를 재정의하고, view 또는 elements를 정의해야 합니다.
  3. self.prepended의 첫 번째로 super를 호출해야 합니다.
  4. 다른 모듈을 포함하거나 추가하고, view/elementsbase.class_eval 블록 내에서 정의해야 하며, 이를 통해 모듈을 추가한 클래스 내에서 정의되도록 해야 합니다.

이 단계는 유효성 검사 선택기가 문제를 제대로 감지하도록 보장합니다.

예를 들어, qa/qa/ee/page/merge_request/show.rbqa/qa/page/merge_request/show.rb에 EE 전용 메서드를 추가하며 ( QA::Page::MergeRequest::Show.prepend_mod_with('Page::MergeRequest::Show', namespace: QA)와 함께) 다음과 같이 구현됩니다 (관련 부분만 보여주며 위에서 설명한 4단계를 주석으로 참조):

module QA
  module EE
    module Page
      module MergeRequest
        module Show
          extend QA::Page::PageConcern # 1.

          def self.prepended(base) # 2.
            super # 3.

            base.class_eval do # 4.
              prepend Page::Component::LicenseManagement

              view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
                element 'head-mismatch', "소스 브랜치 HEAD가 최근에 변경되었습니다."
              end

              [...]
            end
          end
        end
      end
    end
  end
end

로컬에서 테스트 실행하기

개발 중에 qa:selectors 테스트를 실행하려면

bin/qa Test::Sanity::Selectors

qa 디렉터리 내에서 실행합니다.

도움을 요청할 곳은?

더 많은 정보가 필요하면 Slack의 #test-platform 채널에서 도움을 요청하세요
(내부, GitLab 팀 전용).

팀 멤버가 아닌 경우 기여하는 데 도움이 더 필요하면,
~QA 레이블과 함께 GitLab CE 이슈 트래커에 이슈를 열어주세요.