- なぜそれが必要なのでしょうか?
- 過去にどんな問題があったでしょうか?
- どのようにして脆弱なテストの問題を解決したのでしょうか?
- ページオブジェクトを適切に実装するには?
- ローカルでのテストの実行
- どこで助けを求めますか?
GitLab QAのページオブジェクト
GitLab QAでは、_ページオブジェクトと_呼ばれる既知のパターンを使っています。
これは、GitLab QAシナリオを駆動するために使用する、GitLabのすべてのページの抽象化を構築したことを意味します。フォームへの入力やボタンの選択など、ページ上で何かをするときはいつも、GitLabのこの領域に関連付けられたページオブジェクトを通してのみ行います。
例えば、GitLab QAテストハーネスがGitLabにサインインするとき、ユーザーログインとユーザーパスワードを入力する必要があります。そのために、Page::Main::Login
とsign_in_using_credentials
メソッドというクラスがあり、user_login
とuser_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
に更新しなければなりません。
過去にどんな問題があったでしょうか?
パフォーマンス上の理由と、パッケージのビルドとすべてのテストに時間がかかるためです。
誰かが_新しいセッションビューで_ 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
データ属性をビューファイルに追加しなければならない要素を宣言します。
実際のビューコードにマッチする値(文字列または正規表現)を定義することもできますが、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-testid
対data-qa-selector
GitLab 16.1 で導入されました。
ビュー内で要素を定義する方法として、2つの方法がサポートされています。
data-testid
-
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固有のモジュールがプリペンドされているものがあります。
これらのモジュールは
-
QA::Page::PageConcern
モジュールから拡張され、extend QA::Page::PageConcern
。 -
include
/prepend
他のモジュールが必要な場合はself.prepended
メソッドをオーバーライドし、view
またはelements
を定義してください。 -
self.prepended
の最初にsuper
を呼び出します。 - 他のモジュールをインクルード/プリペンドし、
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
ラベルを付けてイシューを開いてください。