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't.text_field 'username'로 변경하면 다른 필드 식별자가 생성되어 모든 테스트가 실패하게 됩니다.

우리는 GitLab에 로그인하기 위해 Page::Main::Login.perform(&:sign_in_using_credentials)를 사용하기 때문에 페이지 객체가 GitLab에 로그인하려는 경우에는 단일 진실 공급원이며, 우리는 fill_in 'user-login'fill_in 'user-username'로 한 곳에서만 업데이트해야 합니다.

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

성능 문제로 인해 모든 커밋에 대해 QA 테스트를 실행하지 않고 있으며, 패키지를 빌드하고 모든 것을 테스트하는 데 걸리는 시간 때문에 그렇게 하지 않고 있습니다.

그래서 누군가가 new session 보기에서 t.text_field 'login't.text_field '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 데이터 속성이 뷰 파일에 추가되어야 함을 선언합니다.

또한 실제 뷰 코드와 일치하는 값을(문자열 또는 정규 표현식) 정의할 수도 있지만 이것은 사용이 중단되었으며 다음 두 가지 이유로 권장되지 않습니다:

  • 일관성: 요소를 정의하는 유일한 방법이 있습니다.
  • 관심사의 분리: 테스트는 다른 컴포넌트에서 재사용되는 코드나 클래스를 재사용하는 대신 전용 data-testid 속성을 사용합니다(예: 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

이러한 요소를 뷰에 추가하려면 각 요소에 대한 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는 일치하고 kebab 케이스여야 합니다.
  • 요소가 무조건적으로 페이지에 나타나는 경우, 요소에 required: true를 추가해야 합니다. Dynamic element validation을 참조하십시오.
  • 페이지 객체에서 data-qa-selector 클래스를 볼 수 없어야 합니다. 우리는 요소 정의에 data-testid 메서드를 사용해야 합니다(data-testid와 관련한).

data-testid vs data-qa-selector

  • GitLab 16.1에서 도입됨

기존의 data-qa-selector 클래스는 사용이 중단되었다고 간주되며 우리는 data-testid 정의 방법을 사용해야 합니다.

동적 요소 선택

자동화된 테스트에서 흔히 발생하는 일은 “여러 요소 중 하나”를 선택하는 것입니다. 디렉터리에서 여러 항목 중에서 어떻게 차이점을 찾을 수 있는지에 대한 자세한 내용은 차후 업데이트하겠습니다.

위 내용을 바탕으로 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

테스트에서 이 특정 이슈가 존재하는지 유효성을 검사할 수 있습니다.

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 선택기를 사용하고, ‘element’를 추가할 수 없는 이유에 대해 설명하는 주석을 달면 합리적입니다.

페이지 관심사 정의

일부 페이지는 공통 동작을 공유하거나/또는 EE(Enterprise Edition)-특정 모듈로 시작되어, EE-특정 메소드를 추가할 수 있습니다.

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

  1. QA::Page::PageConcern 모듈에서 확장하고, extend QA::Page::PageConcern로 확장합니다.
  2. 다른 모듈을 include/prepend해야하는 경우 self.prepended 메소드를 재정의하고, view 또는 elements를 정의합니다.
  3. self.prepended에서 가장 먼저 super를 호출합니다.
  4. 다른 모듈을 include/prepend하고, view/elements를 정의하면서 이러한 클래스의 정의가 확실히 되도록 base.class_eval 블록에 넣습니다.

이러한 단계들은 선택기의 안정성 검사가 문제를 적절하게 감지할 수 있도록 합니다.

예를 들어, qa/qa/ee/page/merge_request/show.rb는 EE-특정 메소드를 qa/qa/page/merge_request/show.rb에 추가합니다 (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

도움을 요청할 수 있는 곳은 어디인가요?

더 많은 정보가 필요한 경우, 내부적으로 GitLab 팀 회원만이 Slack의 #test-platform 채널에서 도움을 요청하세요.

팀 멤버가 아니고 기여에 도움이 필요한 경우, ~QA 라벨을 사용하여 GitLab CE 이슈 트래커에서 이슈를 열어주세요.