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

GitLab QAでは、_ページオブジェクトと_呼ばれる既知のパターンを使っています。

これは、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 :login を後でt.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のビュー、パーシャル、またはVueコンポーネントに対応します。

element DSLメソッドは、対応するtestid=element_name データ属性をビューファイルに追加しなければならない要素を宣言します。

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

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

  ### Good ###

  # Implicitly require the CSS selector `[data-testid="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-testid 属性を追加して、Rails のビュー、パーシャル、または Vue コンポーネントを変更する必要があります。

この例では、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 クラスを既存の Pages Objects で見かけるかもしれません。data-qa-selector CSSクラスよりもdata-testid の定義方法を選ぶべきです。

data-testiddata-qa-selector

GitLab 16.1 で導入されました。

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

  1. data-testid
  2. data-qa-selector 属性

既存のdata-qa-selector クラスはすべて非推奨とみなし、data-testid の方法で定義することを推奨します。

動的要素選択

GitLab 12.5で導入されました。

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

これを回避するために、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) #=> 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. include/prepend 他のモジュールが必要な場合はself.prepended メソッドをオーバーライドし、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_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:selectors テストを実行するには、次のコマンドを実行します。

bin/qa Test::Sanity::Selectors

qa を実行することで実行できます。

どこで助けを求めますか?

より詳しい情報が必要な場合は、Slack の#quality チャンネルで助けを求めてください(内部、GitLab チームのみ)。

チームメンバーでなく、貢献するために助けが必要な場合は、GitLab CE issue tracker で~QA ラベルを付けてイシューを開いてください。