GitLab QAのページオブジェクト

GitLab QAでは、_Pages Objectsと_呼ばれる既知のパターンを使っています。

これは、GitLabのQAシナリオを駆動するために使用するGitLabのすべてのページのための抽象化を構築したことを意味します。 フォームへの入力やボタンのクリックなど、ページ上で何かを行うときはいつでも、GitLabのこの領域に関連付けられたページオブジェクトを介してのみ行います。

例えば、GitLab QAテストハーネスがGitLabにサインインするとき、ユーザーログインとユーザーパスワードを入力する必要があります。 そのために、Page::Main::Loginsign_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 にサインインするときにはページオブジェクトが唯一のソースとなり、fill_in :user_loginfill_in :user_username に更新する必要があります。

過去にどんな問題がありましたか?

パフォーマンス上の理由と、パッケージのビルドとすべてのテストに時間がかかるためです。

そのため、_新しいセッションビューで_誰かがt.text_field :logint.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 View、パーシャル、またはVueコンポーネントに対応します。

element DSLメソッドは、対応するdata-qa-selector=element_name_snaked データ属性をビューファイルに追加する必要がある要素を宣言します。

実際のビューのコードにマッチする値 (文字列あるいは正規表現) を定義することもできますが、 2 つの理由により、この方法は推奨されません:

  • 一貫性: 要素を定義する方法は一つしかありません。
  • 懸念事項の分離:QAは、他のコンポーネントが使用するコードやクラス(例:js-* クラスなど)を再利用する代わりに、専用のdata-qa-* 属性を使用します。
view 'app/views/my/view.html.haml' do

  ### Good ###

  # Implicitly require the CSS selector `[data-qa-selector="logout_button"]` to be present in the view
  element :logout_button

  ### Bad ###

  ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
  # Require `f.submit "Sign in"` to be present in `my/view.html.haml
  element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern

  ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop.
  # Match every line in `my/view.html.haml` against
  # `/link_to .* "My Profile"/` regexp.
  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-qa-selector 属性を追加して、Rails View、パーシャル、または Vue コンポーネントを変更する必要があります。

この場合、data-qa-selector="login_field"data-qa-selector="password_field" 、そしてdata-qa-selector="sign_in_button"

アプリ/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: { qa_selector: 'login_field' }
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' }
= f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' }

注意すべきこと

  • 要素名とqa_selector は一致し、snake_caseされていなければなりません。
  • 要素が無条件にページに表示される場合は、要素にrequired: true を追加します。動的要素検証を参照してください。
  • 既存のPage Objectsに.qa-selector.qa-selector CSSクラスよりも、data-qa-selectorの定義方法を優先すべきです。

data-qa-selector.qa-selector

GitLab 12.1 で導入されました。

ビュー内の要素を定義するには、2つの方法がサポートされています。

  1. data-qa-selector 属性
  2. .qa-selector クラス

既存の.qa-selector クラスはすべて非推奨とみなし、data-qa-selector の定義方法を選ぶべきです。

動的要素選択

GitLab 12.5 で導入されました。

自動テストでありがちなのは、単一の「one-of-many」要素を選択することです。 複数の項目からなるリストで、何を選択するのかを区別するにはどうすればよいでしょうか。 この回避策として最も一般的なのは、テキストマッチングです。 その代わりに、テキストではなく、一意の識別子で特定の要素をマッチングするのがよりよい方法です。

これを回避するために、data-qa-* 拡張可能な選択メカニズムを追加しました。

使用例

例1

次のようなRailsビューがあるとします (例としてGitLabイシューを使用します):

%ul.issues-list
 - @issues.each do |issue|
   %li.issue{data: { qa_selector: '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: { qa_selector: 'model', qa_index: idx } }
expect(the_page).to have_element(:model, index: 1) #=> select on the first model that appears in the list

例外

場合によっては、セレクタを追加することができなかったり、追加する価値がなかったりすることもあります。

UIコンポーネントの中には、サードパーティによってメンテナンスされているものも含め、外部のライブラリを使用しているものがあります。 ライブラリがGitLabによってメンテナンスされている場合でも、セレクタのサニティテストはGitLabプロジェクト内のコードに対してのみ実行されるため、ライブラリ内のコードに対してビューのパスを指定することはできません。

このようなまれなケースでは、ページオブジェクトメソッドでCSSセレクタを使用するのが合理的で、element

ページに関する定義

ページによっては、共通の動作があったり、EE固有のメソッドを追加するEE固有のモジュールが前置されていたりします。

これらのモジュールは

  1. QA::Page::PageConcern モジュールからextend QA::Page::PageConcernで拡張。
  2. self.prepended メソッドをオーバーライドする必要がある場合は、include/prepend 他のモジュール自身をオーバーライドする必要がある場合は、view またはelementsを定義します。
  3. self.prependedで最初にsuper を呼び出します。
  4. 他のモジュールをインクルード/プリペンドし、base.class_eval ブロックでview/elements を定義し、モジュールをプリペンドするクラスで定義されていることを確認します。

これらの手順により、サニティ・セレクター・チェックが適切に問題を検出することが保証されます。

例えば、qa/qa/ee/page/merge_request/show.rb は、qa/qa/page/merge_request/show.rb にEE固有のメソッドを追加しています(QA::Page::MergeRequest::Show.prepend_if_ee('QA::EE::Page::MergeRequest::Show')と一緒です)。以下はその実装方法です(関連する部分のみを示し、上記の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 の#quality チャンネルで助けを求めてください(内部、GitLab チームのみ)。

チームメンバーでなくても貢献したい場合は、GitLab CE issue trackerに~QA のラベルを付けてイシューを登録してください。