- クラスとモジュールの命名
- テストとテストケースのリンク
- テストの命名
- UIよりもAPIを優先
- 余計な期待を避ける
aggregate_failures
、背中合わせの期待がある場合を優先します。- 複数の期待値がある場合は、
aggregate_failures
。 expect do ... raise_error
ブロック内での複数のアクションは避けてください。- テストを複数のファイルに分割することを優先します。
let
変数とインスタンス変数の比較before(:context)
とafter
フックで UI の使用を制限します。- テストがブラウザにログインしたままにならないようにします。
- 管理者アクセスが必要なタグテスト
Commit
リソースを優先してください。ProjectPush
- 要素をぼかすための望ましい方法
-
expect
ステートメントが効率的に待機するようにします。 - puts よりも logger を使いましょう
エンドツーエンドテストのベストプラクティス
これは、テストガイドにあるベストプラクティスを拡張したものです。
クラスとモジュールの命名
QA フレームワークでは、クラスとモジュールのオートロードにZeitwerk を使用します。デフォルトの Zeitwerkインフレクタは、snake_cased ファイル名を PascalCased モジュール名またはクラス名に変換します。手動による屈折のメンテナンスを避けるため、このパターンに従うことをお勧めします。
カスタム屈折ロジックが必要な場合は、loader.inflector.inflect
メソッド呼び出しのqa.rbファイルにカスタム屈折器を追加します。
テストとテストケースのリンク
すべてのテストには、GitLabプロジェクトのテストケースに対応するテストケースがあり、Quality Test Casesプロジェクトに結果のイシューがあるはずです。テストケースのイシューがまだ存在しない場合、GitLabチームのメンバーであれば誰でも CI/CD > テストケースページで新しいテストケースを作成することができます。テストケースの URL がコード内のテストにリンクされた後、レポートが有効になっているパイプラインでテストが実行されると、report-results
スクリプトは自動的にテストケースと結果のイシューを更新 report-results
します。結果report-results
イシューがまだ存在 report-results
しない場合は、スクリプトが自動的にイシューを作成し、対応するテストケースにリンクします。
テストケースをコード内のテストにリンクするには、testcase
RSpec メタデータ タグを手動で追加する必要があります。ほとんどの場合、1 つのテストは 1 つのテストケースに関連付けられています。
使用例:
RSpec.describe 'Stage' do
describe 'General description of the feature under test' do
it 'test name', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/:test_case_id' do
...
end
it 'another test', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/:another_test_case_id' do
...
end
end
end
共有テストの場合
ほとんどのテストは、spec
ファイルの一行で定義されます。そのため、これらのテストは、testcase
タグでひとつのテストケースにリンクすることができます。
しかし、spec
ファイルの 1 行とテストケースが 1 対 1 の関係にないテストもあります。これは、1 つの行が複数のテストに関連付けられるように定義されているテストがあるためです:
- 並列化されたテスト。
- テンプレート化されたテスト
- 複数の例を含む共有例のテスト。
このような場合、他の方法でテストケースのリンクを含める必要があります。
例として、qa/specs/features/ee/browser_ui/3_create/repository/restrict_push_protected_branch_spec.rb
の共有例の中に二つのテストがあります:
RSpec.shared_examples 'unselected maintainer' do |testcase|
it 'user fails to push', testcase: testcase do
...
end
end
RSpec.shared_examples 'selected developer' do |testcase|
it 'user pushes and merges', testcase: testcase do
...
end
end
共有例を含む次のテストを考えてみましょう:
RSpec.describe 'Create' do
describe 'Restricted protected branch push and merge' do
context 'when only one user is allowed to merge and push to a protected branch' do
...
it_behaves_like 'unselected maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347775'
it_behaves_like 'selected developer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347774'
end
context 'when only one group is allowed to merge and push to a protected branch' do
...
it_behaves_like 'unselected maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347772'
it_behaves_like 'selected developer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347773'
end
end
end
各共有例に対して 2 つずつ、合計 4 つのテストケースを作成することを推奨します。
テストの命名
テストの名前は、テストの目的を定義する読みやすい文章にします。私たちのテストガイドは、Thoughtbotのテストスタイルガイドを拡張したものです。このページでは、https://www.betterspecs.org/およびRSpec ネーミングガイドからの情報とともに、ガイドラインを説明します。
推奨する方法
次のブロックはPlan wiki content creation in a project adds a home page
# `RSpec.describe` is the DevOps Stage being covered
RSpec.describe 'Plan', product_group: :knowledge do
# `describe` is the feature being tested
describe 'wiki content creation' do
# `context` provides the condition being covered
context 'in a project'
# `it` defines the expected result of the test
it 'adds a home page'
...
end
...
end
...
end
end
-
describe
、context
、it
の各ブロックには、短い説明を付けなければなりません。 - 説明はできるだけ簡潔にしてください。
- 長い説明文や複数の条件文は、分割すべきサインかもしれません (
context
ブロックの追加)。 - ドキュメンテーション・スタイル・ガイド』では、簡潔でアクティブボイスを使った書き方を推奨しています。
- 長い説明文や複数の条件文は、分割すべきサインかもしれません (
- 一番外側の
Rspec.describe
ブロックは、DevOps のステージ名であるべきです。 -
Rspec.describe
ブロックの内部にはdescribe
ブロックがあり、テスト対象の機能名が記述されています。 - オプションの
context
ブロックは、テストされる条件を定義します。-
context
ブロックの説明は、when
,with
,without
,for
,and
,on
,in
,as
, またはif
で始まる必要があります。
-
-
it
ブロックには、テストの合否基準を記述します。-
shared_examples
、it
ブロックの代わりに、specify
ブロックを使用することができます。
-
UIよりもAPIを優先
エンドツーエンドのテストフレームワークは、ケースバイケースでリソースを作成する能力を持っています。リソースは可能な限りAPI経由で作成されるべきです。
テストが必要とするリソースを API 経由で作成することで、時間とコストの両方を節約できます。
リソースについて詳しくはこちら。
余計な期待を避ける
テストを無駄のないものにするためには、必要なものだけをテストすることが重要です。
テストする必要のないexpect()
ステートメントを追加しないようにしましょう。
使用例:
#=> Good
Flow::Login.sign_in
Page::Main::Menu.perform do |menu|
expect(menu).to be_signed_in
end
#=> Bad
Flow::Login.sign_in(as: user)
Page::Main::Menu.perform do |menu|
expect(menu).to be_signed_in
expect(page).to have_content(user.name) #=> we already validated being signed in. redundant.
expect(menu).to have_element(:nav_bar) #=> likely unnecessary. already validated in lower-level. test doesn't call for validating this.
end
#=> Good
issue = create(:issue, name: 'issue-name')
Project::Issues::Index.perform do |index|
expect(index).to have_issue(issue)
end
#=> Bad
issue = create(:issue, name: 'issue-name')
Project::Issues::Index.perform do |index|
expect(index).to have_issue(issue)
expect(page).to have_content(issue.name) #=> page content check is redundant as the issue was already validated in the line above.
end
aggregate_failures
、背中合わせの期待がある場合を優先します。
複数の期待値がある場合、プリファード・アグリゲートの失敗を参照してください。
複数の期待値がある場合は、aggregate_failures
。
テストケース内に複数の期待値が必要な場合は、aggregate_failures
を使用することをお勧めします。
これにより、最初の失敗でテストが中断されるのではなく、期待値のセットをグループ化し、すべての失敗をまとめて見ることができます。
使用例:
#=> Good
Page::Search::Results.perform do |search|
search.switch_to_code
aggregate_failures 'testing search results' do
expect(search).to have_file_in_project(template[:file_name], project.name)
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
#=> Bad
Page::Search::Results.perform do |search|
search.switch_to_code
expect(search).to have_file_in_project(template[:file_name], project.name)
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
複数の期待がステートメントで区切られている場合、:aggregate_failures
メタデータを例に添付します。
#=> Good
it 'searches', :aggregate_failures do
Page::Search::Results.perform do |search|
expect(search).to have_file_in_project(template[:file_name], project.name)
search.switch_to_code
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
#=> Bad
it 'searches' do
Page::Search::Results.perform do |search|
expect(search).to have_file_in_project(template[:file_name], project.name)
search.switch_to_code
expect(search).to have_file_with_content(template[:file_name], content[0..33])
end
end
expect do ... raise_error
ブロック内での複数のアクションは避けてください。
複数のアクションを1つのexpect do ... end.not_to raise_error
またはexpect do ... end.to raise_error
ブロックにまとめると、ログが出力されるため、失敗の実際の原因をデバッグするのが難しくなります。重要な情報が切り捨てられたり、完全に欠落したりする可能性があります。
たとえば、expect_owner_permissions_allow_delete_issue
のように、いくつかのアクションと期待値をテストの非公開メソッドにカプセル化します:
it "has Owner role with Owner permissions" do
Page::Dashboard::Projects.perform do |projects|
projects.filter_by_name(project.name)
expect(projects).to have_project_with_access_role(project.name, 'Owner')
end
expect_owner_permissions_allow_delete_issue
end
そして、そのメソッド自体に
#=> Good
def expect_owner_permissions_allow_delete_issue
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end
#=> Bad
def expect_owner_permissions_allow_delete_issue
expect do
issue.visit!
Page::Project::Issue::Show.perform(&:delete_issue)
Page::Project::Issue::Index.perform do |index|
expect(index).not_to have_issue(issue)
end
end.not_to raise_error
end
テストを複数のファイルに分割することを優先します。
私たちのフレームワークには、spec ファイルを並列に実行するいくつかの並列化メカニズムがあります。
しかし、テストは specファイル単位で並列化され、test/example 単位では並列化されないため、既存のファイルに新しいテストが追加されても、より大きな並列化は実現できません。
とはいえ、既存のファイルに新しいテストを追加する理由は他にも考えられます。
たとえば、セットアップにコストのかかる状態をテストが共有している場合、たとえそのセットアップを使用するテストが並列化できないとしても、そのセットアップを一度実行したほうが効率的な場合があります。
まとめると
- To-Do:高価なセットアップを共有するテストを除き、テストを別々のファイルに分割します。
- やめてください:並列化への影響を考慮せずに、既存のファイルに新しいテストを入れること。
let
変数とインスタンス変数の比較
デフォルトでは、let
またはインスタンス変数を使用する場合は、テストのベストプラクティスに従います。しかし、エンドツーエンドのテストでは、リソースの作成などのセットアップにコストがかかります。let
を使ってリソースを保存すると、各サンプルに対して個別に作成されます。リソースを複数の例で共有できる場合は、before(:all)
ブロックのインスタンス変数を代わりにlet
使用して、実行時間を節約 let
します。let
変数を複数の例で共有できない場合は、. let
before(:context)
とafter
フックで UI の使用を制限します。
before(:context)
フックの使用は、APIコール、非UIオペレーション、またはログインのような基本的なUIオペレーションのみでセットアップタスクを実行するように制限してください。
失敗時に自動的にスクリーンショットを保存するためにcapybara-screenshot
ライブラリを使用します。
capybara-screenshot
はスクリーンショットを RSpec のafter
フックに保存します。 before(:context)
で失敗した場合、after
フックは に呼び出されないため、スクリーンショットは保存されません。
このことから、before(:context)
の使用は、スクリーンショットが不要なオペレーションのみに限定すべきです。
同様に、このafter
フックはUI以外のオペレーションにのみ after
使うべきです。テストファイルのフックにafter
あるUIオペレーションはすべて after
、スクリーンショットを撮るafter
フックの前に実行されます。これは、UIステータスを障害発生地点から遠ざける結果となり、スクリーンショットが適切なタイミングでキャプチャされません。
テストがブラウザにログインしたままにならないようにします。
すべてのテストは、テストの開始時にサインインできることを期待します。
例については、イシュー#34736 を参照してください。
after(:context)
(あるいはbefore(:context)
) ブロックで実行されるアクションは、API を使って実行されるのが理想的です。ユーザーインターフェイスで行う必要がある場合 (例えばAPI機能が存在しない場合) は、必ずブロックの最後でサインアウトしてください。
after(:all) do
login unless Page::Main::Menu.perform(&:signed_in?)
# Do something while logged in
Page::Main::Menu.perform(&:sign_out)
end
管理者アクセスが必要なタグテスト
管理者アクセスを必要とするテストを本番環境に対して実行することはありません。
管理者アクセスを必要とするテストを新規に追加する場合は、RSpec メタデータ:requires_admin
を適用して、Production 環境やその他のテストを実行したくない環境に対して実行されるテストスイートに、そのテストが含まれないようにします。
ローカルでテストを実行する場合やパイプラインを設定する場合は、環境変数QA_CAN_TEST_ADMIN_FEATURES
をfalse
に設定することで、:requires_admin
タグを持つテストをスキップすることができます。
feature_flag
タグを使用してください。詳細は機能フラグを使ったテストにあります。
Commit
リソースを優先してください。ProjectPush
APIの使用と同様に、可能な限りCommit
のリソースを使用してください。
ProjectPush
Git コマンドラインインターフェイス(CLI) を使った生の Shell コマンドを使うのに対して、Commit
リソースは HTTP リクエストを行います。
# Using a commit resource
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.commit_message = 'Initial commit'
commit.add_files([
{ file_path: 'README.md', content: 'Hello, GitLab' }
])
end
# Using a ProjectPush
Resource::Repository::ProjectPush.fabricate! do |push|
push.commit_message = 'Initial commit'
push.file_name = 'README.md'
push.file_content = 'Hello, GitLab'
end
ProjectPush
を使う例外としては、SSH のインテグレーションや Git CLI をテストする場合などがあります。
要素をぼかすための望ましい方法
要素をぼかすには、テスト状態を変更しない別の要素を選択するのが好ましい方法です。いくつかのドロップダウンで発生する可能性があるように、ページ要素をブロックするマスクがある場合、WebDriver のネイティブマウスイベントを使用して、要素の座標上のクリックイベントをシミュレートします。次のメソッドを使用してください:click_element_coordinates
.
ビューポートの中心をクリックするため、入力やドロップダウンなどのぼかし要素のbody
をクリックすることは避けてください。このアクションは、意図せずに他の要素をクリックし、テストの状態を変更して失敗させる可能性もあります。
# Clicking another element to blur an input
def add_issue_to_epic(issue_url)
find_element(:issue_actions_split_button).find('button', text: 'Add an issue').click
fill_element(:add_issue_input, issue_url)
# Clicking the title blurs the input
click_element(:title)
click_element(:add_issue_button)
end
# Using native mouse click events in the case of a mask/overlay
click_element_coordinates(:title)
expect
ステートメントが効率的に待機するようにします。
一般的に、expect
ステートメントを使用して、期待_通りか_どうかをチェックします。例えば
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).to have_job('a_job')
end
待つ必要のある期待値にはeventually_
マッチャーを使います。
マッチングに待ち時間が必要な場合は、待ち時間を明確に定義したeventually_
マッチャーを使用してください。
Eventually
matchers は以下の命名パターンを使います:eventually_${rspec_matcher_name}
。これらはeventually_matcher.rbで定義されています。
expect { async_value }.to eventually_eq(value).within(max_duration: 120, max_attempts: 60, reload_page: page)
expect
チェックを高速化するために、否定可能なマッチャを作成します。
しかし、時には、あるものが_ない_ことを確認したいこともあります。つまり、何かがないことを確認したいのです。ユニットテストや機能仕様では、not_to
を使うのが一般的です。RSpec の組み込み matcher は否定可能で、Capybara の matcher も否定可能です。
except(page).not_to have_text('hidden')
except(page).to have_no_text('hidden')
残念ながら、ページオブジェクトに追加する述語メソッドでは、自動的にそうなるわけではありません。否定可能なマッチャを独自に作成する必要があります。
最初の例では、Page::Project::Pipeline::Show
ページオブジェ ク ト](https://gitlab.com/gitlab-org/gitlab/-/blob/87864b3047c23b4308f59c27a3757045944af447/qa/qa/page/project/pipeline/show.rb#L53) の[has_job?
述語 メ ソ ッ ド か ら 派生 し たhave_job
マ ッ チ ャ ー を用いてい ます。否定可能なマッチャーを作るには、has_no_job?
を否定ケースに使います:
RSpec::Matchers.define :have_job do |job_name|
match do |page_object|
page_object.has_job?(job_name)
end
match_when_negated do |page_object|
page_object.has_no_job?(job_name)
end
end
そして、次の例の2つのexpect
ステートメントは等価です:
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job')
expect(pipeline).to have_no_job('a_job')
end
カスタムマッチャーを追加する実際の例については、このマージリクエストを参照してください。
私たちはqa/spec/support/matchers
で否定可能なカスタムマッチャーを作成しています。
not_to
を使用する場合のみ、否定可能なマッチャを作成する必要があります。to have_no_*
を使用する場合、否定可能なマッチャーは必要ありませんが、コードの可読性を高めます。否定可能なマッチャが必要な理由
以下のコードを考えてみましょう。ただし、have_job
に対して否定可能なマッチャーをカスタムで用意していないと仮定します_。_
# Bad
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job')
end
この文が通るためには、have_job('a_job')
がfalse
を返し、not_to
がそれを否定できるようにしなければなりません。問題は、have_job('a_job')
がfalse
を返す前に、'a job'
が現れるまで最大10秒待つことです。想定される条件では、このテストは必要以上に10秒長くかかります。
その代わりに、待ち時間を発生させないようにすることもできます:
# Not as bad but potentially flaky
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).not_to have_job('a_job', wait: 0)
end
問題は、'a_job'
が存在し、それが消えるのを待っている場合、この文は失敗するということです。
has_no_job?
述語メソッドが使用され、ジョブが消滅するのに必要な時間だけ待つからです。
最後に、否定可能なマッチャは、have_no_*
という形式のマッチャを使用するよりも好まれます。なぜなら、not_to
を使用してマッチャを否定することは、一般的でよく知られた習慣だからです。否定可能なマッチャを追加することで、そのような慣行を容易にすれば、後続のテスト作成者が効率的なテストを書きやすくなります。
puts よりも logger を使いましょう
現在、GitLab QAアプリケーションとエンドツーエンドテストの両方でログを処理するためにRailslogger
を使っています。これは、puts
と比較して、以下のような追加機能を提供します:
- ロギングレベルを指定する機能。
- 類似ログにタグを付ける機能。
- ログメッセージの自動フォーマット