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'으로 변경하면 다른 필드 식별자를 생성하게 되어 모든 테스트를 실패하게 만들 수 있습니다.

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 nightly 파이프라인이 실패하거나 누군가가 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 코프에 의해 금지됩니다.
  # `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가 일치해야 하며 kebab 케이스여야 합니다.
  • 요소가 무조건적으로 페이지에 표시되는 경우 요소에 required: true를 추가해야 합니다. 동적 요소 유효성 검사 참조
  • 페이지 객체에 data-qa-selector 클래스를 사용해서는 안 됩니다. data-testid 정의 방법을 사용해야 합니다.

data-testiddata-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

이를 통해 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 프로젝트 내의 코드에서만 실행되므로 라이브러리 내 코드에 대한 뷰 경로를 지정할 수 없습니다.

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

페이지 관련 사항 정의

일부 페이지는 공통 동작을 공유하거나 EE(Enterprise Edition)별 모듈이 추가된 경우가 있습니다.

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

  1. QA::Page::PageConcern 모듈에서 확장되어야 하며, extend QA::Page::PageConcern를 사용해야 합니다.
  2. 자체 모듈을 include/prepend하거나 view 또는 elements을 정의해야 하는 경우 self.prepended 메서드를 재정의해야 합니다.
  3. self.prepended 내에서 가장 먼저 super를 호출해야 합니다.
  4. 기타 모듈을 base.class_eval 블록 내에 include/prepend하고 그들의 view/elements를 정의하여 모든 문제가 올바르게 감지되도록 해야 합니다.

위 단계를 통해 선택기의 안정성을 확인할 수 있습니다.

예를 들어, qa/qa/ee/page/merge_request/show.rb에서 qa/qa/page/merge_request/show.rb에 EE별 메서드를 추가하는 경우(QA::Page::MergeRequest::Show.prepend_mod_with('Page::MergeRequest::Show', namespace: QA)를 통해), 다음은 해당 방법의 구현 방법입니다.

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 채널에서 도움을 요청하세요.

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