GitLab QA의 페이지 객체

GitLab QA에서는 _페이지 객체_라는 알려진 패턴을 사용합니다.

이는 GitLab의 모든 페이지에 대한 추상화를 구축하여 GitLab QA 시나리오를 구동하는 데 사용하는 의미입니다. 페이지에서 어떤 작업을 수행할 때(예: 양식 작성 또는 버튼 선택), GitLab의 해당 영역에 연결된 페이지 객체를 통해서만 작업합니다.

예를 들어, GitLab QA 테스트 하네스가 GitLab에 로그인할 때, 사용자 로그인 및 사용자 비밀번호를 입력해야 합니다. 이를 위해 우리는 Page::Main::Login 클래스와 sign_in_using_credentials 메서드를 사용합니다. 이 코드 조각은 user_loginuser_password 필드를 읽는 유일한 코드입니다.

왜 그것이 필요한가요?

페이지 객체가 필요한 이유는 중복을 줄이고 GitLab 소스 코드의 선택기를 변경할 때 문제를 피해야하기 때문입니다.

GitLab QA에서 100개의 사양이 있다고 상상해보세요. 우리는 단언을 하기 전에 매번 GitLab에 로그인해야 합니다. 페이지 객체가 없으면, 불안정한 도우미에 의존하거나 직접 Capybara 메서드를 호출해야 합니다. 매 *_spec.rb 파일/테스트 예제에서 fill_in :user_login을 호출하는 것을 상상해보세요.

이 페이지와 연결된 보기에서 t.text_field :login을 나중에 다른 필드 식별자로 변경하면 모든 테스트가 실패합니다.

우리는 GitLab에 로그인하려면 어디서나 Page::Main::Login.perform(&:sign_in_using_credentials)을 사용하기 때문에 페이지 객체는 단일한 진실의 근원이며, fill_in :user_loginfill_in :user_username로 변경해야하는 것은 한 곳에서만 되어야 합니다.

지난 문제점은 무엇이었나요?

성능 상의 이유로 매 커밋마다 QA 테스트를 실행하지 않았고, 모든 것을 빌드하고 테스트하는 데 걸리는 시간 때문에 그렇습니다.

이것이 바로 어떤 사람이 ‘login’을 ‘username’으로 변경할 때 새 세션 보기에서 우리가 해당 변경에 대해 알지 못하는 이유입니다. 우리의 GitLab QA 야간 파이프라인이 실패하거나 누군가가 자신의 병합 요청에서 package-and-qa 액션을 트리거할 때까지 우리는 이 변경 사실을 모르게 됩니다.

이러한 변경은 모든 테스트를 실패시킬 것입니다. 우리는 이 문제를 “취약한 테스트 문제”라고 부릅니다.

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

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

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

CE/EE 저장소에 코드를 푸시할 때마다 qa:selectors 산정성 테스트 작업이 CI 파이프라인의 일부로 실행됩니다.

이 테스트는 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 데이터 속성을 선언합니다.

또한 실제 뷰 코드에 대한 값(String 또는 Regexp)을 일치시킬 수 있지만, 이것은 폐지되었습니다. 그 이유는 다음과 같습니다.

  • 일관성: 요소를 정의하는 유일한 방법이 있습니다.
  • 관심사의 분리: QA는 다른 구성 요소에 의한 코드 또는 클래스를 재사용하는 대신 전용 data-qa-* 속성을 사용합니다(예: js-* 클래스 등).
view 'app/views/my/view.html.haml' do

  ### 좋은 예시 ###

  # 해당 뷰에 CSS 선택기 `[data-testid="logout_button"]`가 있음을 암묵적으로 요구합니다.
  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 속성을 추가해야 합니다.

우리의 경우, 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: "This field is required.", data: { testid: 'login_field' }
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { testid: 'password_field' }
= f.submit "Sign in", class: "btn btn-confirm", data: { testid: 'sign_in_button' }

주의할 점:

  • 요소의 이름과 data-testid는 일치해야 하며, 스네이크 케이스 또는 케밥 케이스 중 하나여야 합니다.
  • 요소가 무조건적으로 페이지에 나타난다면, 요소에 required: true를 추가해야 합니다. Dynamic element validation 참고
  • 페이지 객체에 data-qa-selector 클래스를 보면 안 됩니다. data-testid 방식을 사용해야 합니다.

data-testid vs data-qa-selector

  • GitLab 16.1에서 도입됨

기존의 data-qa-selector 클래스는 사용이 중단되어야 하며, 대신 data-testid 방식을 사용해야 합니다.

동적 요소 선택

  • GitLab 12.5에서 도입됨

자동화된 테스트에서 흔히 발생하는 경우는 “다수 중의 하나” 요소를 선택하는 것입니다. 목록의 여러 항목 중에서 어떻게 선택할까요? 가장 일반적인 해결책은 텍스트 일치를 통해 하는 것입니다. 대신, 더 좋은 실천법은 고유 식별자를 통해 해당 요소를 선택하는 것입니다.

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

예시

예시 1

다음과 같은 Rails 뷰가 주어진 경우(GitLab 이슈를 예시로 함):

%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(Enterprise Edition)-특정 모듈이 프리픽스로 추가됩니다.

이러한 모듈은 다음을 해야합니다:

  1. QA::Page::PageConcern 모듈에서 확장되어야 하며, extend QA::Page::PageConcern를 해야 합니다.
  2. 필요하다면 self.prepended 메서드를 재정의하고 자체적으로 다른 모듈을 include/prepend하거나 view 또는 elements를 정의해야 합니다.
  3. self.prepended에서 가장 먼저 super를 호출해야 합니다.
  4. 다른 모듈을 base.class_eval 블록 내에서 include/prepend하고 그들의 view/elements를 정의하여 해당 모듈이 prepend된 클래스에 정의되도록 해야 합니다.

이러한 단계는 선택자 안정성 검사가 문제를 제대로 감지하도록 합니다.

예를 들어, qa/qa/ee/page/merge_request/show.rbqa/qa/page/merge_request/show.rb(with QA::Page::MergeRequest::Show.prepend_mod_with('Page::MergeRequest::Show', namespace: QA))에 EE-specific 메서드를 추가하고, 다음은 어떻게 구현되었는지입니다 (관련 부분만 표시하고 위에서 설명한 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, "The source branch HEAD has recently changed."
              end

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

로컬에서 테스트 실행

개발 중에는 qa:selectors 테스트를 다음과 같이 실행할 수 있습니다.

bin/qa Test::Sanity::Selectors

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

도움을 요청할 위치

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

만약 팀 멤버가 아니지만 기여에 도움이 필요하다면, ~QA 라벨이 달린 GitLab CE 이슈 트래커에 이슈를 열어주세요.