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에 100여 개의 스펙이 있다고 가정해보세요. 우리는 단언을 하기 전에 매번 GitLab에 로그인해야 합니다. 페이지 오브젝트가 없으면 불안정한 도우미에 의존하거나 직접 Capybara 메서드를 호출해야 합니다. 매 *_spec.rb 파일 / 테스트 예제에서 fill_in :user_login을 호출한다고 상상해보세요.

나중에 누군가가 이 페이지와 관련된 뷰에서 t.text_field :logint.text_field :username으로 변경하면 다른 필드 식별자가 생성되어 모든 테스트에 영향을 미칩니다.

우리는 여기저기서 Page::Main::Login.perform(&:sign_in_using_credentials)을 사용하여 GitLab에 로그인하고자 합니다. GitLab에 로그인하려면 이 페이지 오브젝트는 단일한 진실의 원천이며, fill_in :user_loginfill_in :user_username로 변경해야 하는 곳은 한 곳 뿐입니다.

과거에 어떤 문제가 있었나요?

성능 문제로 인해 모든 커밋에 대해 QA 테스트를 실행하지 않기 때문에 모든 것을 빌드하고 테스트하기에는 시간이 오래 걸립니다.

이것이 바로 누군가가 t.text_field :logint.text_field :username로 변경할 때 우리가 새로운 세션 뷰에서 이 변경 사항을 알지 못하는 이유입니다. 그래서 우리는 GitLab QA 내부 파이프라인이 실패할 때까지 또는 누군가가 자신의 Merge Request에서 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 데이터 속성을 뷰 파일에 추가해야 한다는 것을 선언합니다.

또한 실제 뷰 코드와 일치하는 값을(문자열 또는 정규식) 정의할 수도 있지만 이것은 폐기되었으며 아래의 이유로 권장되지 않습니다:

  • 일관성: 요소를 정의하는 유일한 방법이 있습니다.
  • 관심사 분리: QA는 다른 컴포넌트에서 사용되는 코드 또는 클래스를 재사용하는 대신 전용 data-qa-* 속성을 사용합니다(예: js-* 클래스 등).
view 'app/views/my/view.html.haml' do
  
  ### 좋음 ###
  
  # 뷰에 `[data-testid="logout_button"]` CSS 선택기가 있어야 함을 암시적으로 요구
  element :logout_button
  
  ### 나쁨 ###
  
  ## 이것은 폐기되었으며 `QA/ElementWithPattern` RuboCop cop에 의해 금지됩니다.
  # `my/view.html.haml`에서 `f.submit "Sign in"`이 있어야 함을 요구
  element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern
  
  ## 이것은 폐기되었으며 `QA/ElementWithPattern` RuboCop cop에 의해 금지됩니다.
  # `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

이러한 요소를 뷰에 추가하려면 각 정의된 요소에 대해 Rails 뷰, 부분 또는 Vue 컴포넌트를 변경하여 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를 추가해야 합니다. 동적 요소 유효성 검사 참조
  • 페이지 오브젝트에 data-qa-selector 클래스가 표시되지 않아야 합니다. 우리는 정의를 위해 data-testid 방법을 사용해야 합니다.

data-testiddata-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 컴포넌트는 서드 파티가 유지 관리하는 외부 라이브러리를 사용합니다. 라이브러리가 GitLab에서 유지 관리되더라도, 선택기 검사는 GitLab 프로젝트 내의 코드에서만 실행되므로 라이브러리의 코드를 위한 뷰 경로를 지정할 수 없습니다.

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

페이지 관심 사항 정의

일부 페이지는 일반 동작을 공유하거나/또는 EE(Enterprise Edition)별 메서드를 추가하는 가능성이 있습니다.

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

  1. QA::Page::PageConcern 모듈에서 확장되어야 합니다. extend QA::Page::PageConcern로 확장됩니다.
  2. self.prepended 메서드를 오버라이드하고, 자체적으로 include/prepend를 해야 하거나, view 또는 elements를 정의해야 하면 오버라이드해야 합니다. self.prepended에서 가장 먼저 super를 호출해야 합니다.
  3. 다른 모듈을 포함하거나, view/elements를 정의하고, 이러한 모듈이 클래스에 정의되도록 하기 위해 base.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, "The source branch HEAD has recently changed."
              end
              
              [...]
            end
          end
        end
      end
    end
  end
end

로컬에서 테스트 실행

개발 중에 qa 디렉터리에서 다음을 실행하여 qa:selectors 테스트를 실행할 수 있습니다.

bin/qa Test::Sanity::Selectors

도움을 요청할 곳은?

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

팀 멤버가 아니라도 기여에 도움이 필요한 경우에는 GitLab CE 이슈 트래커에 ~QA 라벨이 달린 이슈를 개설하십시오.