- テスト設計
- RSpec
テストのベストプラクティス
テスト設計
GitLabにおけるテストは後付けではなく、第一級の市民です。機能の設計と同じように、テストの設計も考慮することが重要です。
機能を実装するときは、正しい機能を正しい方法で開発することを考えます。そうすることで、スコープを管理可能なレベルにまで絞り込むことができます。ある機能のテストを実装する際には、正しいテストを開発することを考えなければなりませんが、その際、テストが失敗する可能性のある重要な方法を_すべて_カバーしなければなりません。これでは、すぐにスコープが広がり、管理しにくいレベルになってしまいます。
テストヒューリスティックは、この問題を解決するのに役立ちます。テストヒューリスティックは、 バグがコードに現れる一般的な方法の多くに簡潔にアドレスします。テストを設計する際には、既知のテストヒューリスティックをレビューしてテスト設計に役立てましょう。ハンドブックの「テストエンジニアリング」のセクションに、いくつかの有用なヒューリスティックスが記載されています。
RSpec
RSpec テストを実行します:
# run test for a file
bin/rspec spec/models/project_spec.rb
# run test for the example on line 10 on that file
bin/rspec spec/models/project_spec.rb:10
# run tests matching the example name has that string
bin/rspec spec/models/project_spec.rb -e associations
# run all tests, will take hours for GitLab codebase!
bin/rspec
Guardを使用して変更を継続的に監視し、一致するテストのみを実行します:
bundle exec guard
spring と guard を併用する場合は、spring の代わりにSPRING=1 bundle exec guard
を使ってください。
一般的なガイドライン
- トップレベルの
RSpec.describe ClassName
。 - クラスメソッドの記述には
.method
を使い、インスタンスメソッドの記述には#method
を使います。 -
context
を使用してブランチロジックをテストします (RSpec/AvoidConditionalStatements
Rubocop Cop -MR)。 - テストの順序をクラスの順序に合わせるようにしてください。
- フェーズを区切るために改行を使用し、4フェーズテストのパターンに従うようにしてください。
-
'localhost'
をハードコーディングするのではなく、Gitlab.config.gitlab.host
を使ってください。 - シーケンスで生成された属性の絶対値に対してアサートしないでください(Gotchas参照)。
-
expect_any_instance_of
やallow_any_instance_of
を使わないようにしましょう (Gotchasを参照)。 -
:each
はデフォルトなので、フックに指定しないでください。 -
before
とafter
のフックでは、:all
よりも:context
にスコープされることを優先してください。 - 指定された要素に作用する
evaluate_script("$('.js-foo').testSomething()")
(またはexecute_script
) を使う場合は、あらかじめCapybara matcher (find('.js-foo')
など) を使って、その要素が実際に存在することを確認してください。 -
focus: true
を使って、実行したい仕様の一部を分離してください。 - テストに複数の期待値がある場合は、
:aggregate_failures
を使用してください。 -
空のテスト記述ブロックの場合、テストが自明であれば
it do
ではなくspecify
を使用してください。 - 実際には存在しないID/IID/アクセスレベルが必要な場合は、
non_existing_record_id
/non_existing_record_iid
/non_existing_record_access_level
を使用してください。123、1234、あるいは 999 を使うのはもろいです。これらの ID は CI を実行するコンテキストで実際にデータベースに存在する可能性があるからです。
アプリケーションコードの読み込み
デフォルトでは、アプリケーションコードは
-
test
環境では、アプリケーション・コードは読み込まれません。 - CI/CD (
ENV['CI'].present?
の場合) では、潜在的なロードの問題を表面化するために、eagerly にロードされます。
テストの実行時に読み込みを有効にする必要がある場合は、GITLAB_TEST_EAGER_LOAD
環境変数を使用してください:
GITLAB_TEST_EAGER_LOAD=1 bin/rspec spec/models/project_spec.rb
ロードされるすべてのアプリケーションコードにテストが依存する場合は、:eager_load
タグを追加してください。これにより、テスト実行前にアプリケーションコードがイーガーロー ドされるようになります。
Ruby の警告
GitLab 13.7 で導入されました。
spec実行時に非推奨の警告をデフォルトで有効にしました。これらの警告を開発者に見やすくすることで、Rubyの新しいバージョンへのアップグレードが容易になります。
環境変数SILENCE_DEPRECATIONS
を設定することで、非推奨の警告を消すことができます:
# silence all deprecation warnings
SILENCE_DEPRECATIONS=1 bin/rspec spec/models/project_spec.rb
テスト順序
GitLab 15.4で導入されました。
テスト順序に依存する欠陥テストを表面化するために、すべての新しい spec ファイルをランダムな順序で実行します。
ランダム化した場合
- 文字列
# order random
は、例のグループの説明の下に追加されます。 - 使用されたシードは、テスト・スイートの要約の下の spec 出力に表示されます。例えば、
Randomized with seed 27443
。
定義された順序で実行される spec ファイルのリストについては、rspec_order_todo.yml
を参照してください。
仕様ファイルをランダムな順序で実行させるには、順序依存関係をチェックしてください:
scripts/rspec_check_order_dependence spec/models/project_spec.rb
チェックに合格すると、スクリプトは自動的にrspec_order_todo.yml
からそれらを削除します。
チェックに失敗した場合は、ランダムな順番で実行する前に修正する必要があります。
テスト速度
GitLabには膨大なテストスイートがあり、並列化しなければ実行に数時間かかることもあります。正確で効果的_かつ_高速なテストを書く努力をすることが重要です。
テストのパフォーマンスは品質と速度をメンテナーする上で重要であり、CIのビルド時間、ひいては固定コストに直接影響します。私たちは、徹底的で、正しく、速いテストを望んでいます。ここでは、そのためのツールやテクニックを紹介します。
必要のない機能を要求しないでください
私たちは、例や親コンテキストに注釈をつけることで、例に機能を簡単に追加できるようにしています。これらの例は次のとおりです:
-
:js
完全なJavaScriptが可能なヘッドレスブラウザを実行するfeature specsにあります。 -
:clean_gitlab_redis_cache
これはサンプルにクリーンなRedisキャッシュを提供します。 -
:request_store
これはサンプルにリクエストストアを提供します。
テストの依存関係を減らすべきですし、機能を避けることでセットアップの量も減らすことができます。
:js
を避けることは特に重要です。これは、機能テストがブラウザでのJavaScriptの反応性を必要とする場合(たとえば、Vue.jsコンポーネントのクリックなど)にのみ使用する必要があります。ヘッドレスブラウザを使用すると、アプリからのHTMLレスポンスを解析するよりもはるかに遅くなります。
プロファイリング: テストがどこに時間を費やしているかを確認します。
rspec-stackprof
を使用すると、テストがどこに時間を費やしたかを示すフレームグラフを作成できます。
この gem は JSON レポートを生成し、https://www.speedscope.app にアップロードしてインタラクティブに可視化できます。
インストール
stackprof
gem はGitLab にインストール済みで、JSON レポートを生成するスクリプト (bin/rspec-stackprof
) も用意されています。
# Optional: install the `speedscope` package to easily upload the JSON report to https://www.speedscope.app
npm install -g speedscope
JSON レポートの生成
bin/rspec-stackprof --speedscope=true <your_slow_spec>
# There will be the name of the report displayed when the script ends.
# Upload the JSON report to speedscope.app
speedscope tmp/<your-json-report>.json
フレームグラフの見方
以下は、フレームグラフを解釈し、ナビゲートするためのいくつかの有用なヒントです:
- flamegraphには、いくつかのビューが用意されています。
Left Heavy
、特に多くの関数呼び出しがある場合に便利です(例えば、機能仕様)。 - ズームイン、ズームアウトが可能です!ナビゲーションのドキュメントを参照してください。
- 遅い機能のテストに取り組んでいる場合、検索で
Capybara::DSL#
を検索すると、行われたCapybaraアクションとその所要時間を見ることができます!
解析例については#414929や #375004を参照してください。
ファクトリー使用の最適化
テストが遅くなる一般的な原因は、オブジェクトの過剰な生成、つまり計算と DB の時間です。ファクトリは開発者にとって必要不可欠ですが、DBへのデータ挿入をとても簡単にすることができるので、最適化できるかもしれません。
ここで留意すべき2つの基本的なテクニックがあります:
- 削減:オブジェクトを作らないこと、そしてオブジェクトを永続化しないこと。
- 再利用:共有オブジェクト、特に私たちが調査しないネストされたオブジェクトは、一般的に共有することができます。
作成を避けるためには、次のことを覚えておくとよいでしょう:
-
instance_double
とspy
はFactoryBot.build(...)
よりも速い。 -
FactoryBot.build(...)
および.build_stubbed
は.create
よりも高速です。 -
build
,build_stubbed
,attributes_for
,spy
,instance_double
を使用できる場合は、create
を使用しないでください。データベースの永続化は遅いです!
ファクトリードクターを使用して、特定のテストでデータベースの永続化が不要なケースを見つけましょう。
# run test for path
FDOC=1 bin/rspec spec/[path]/[to]/[spec].rb
よくある変更は、create
の代わりにbuild
またはbuild_stubbed
を使うことです:
# Old
let(:project) { create(:project) }
# New
let(:project) { build(:project) }
ファクトリープロファイラーは、ファクトリーを介して繰り返されるデータベースの永続化を特定するのに役立ちます。
# run test for path
FPROF=1 bin/rspec spec/[path]/[to]/[spec].rb
# to visualize with a flamegraph
FPROF=flamegraph bin/rspec spec/[path]/[to]/[spec].rb
作成されるファクトリの数が多い一般的な原因は、ファクトリが関連付けを作成および再作成するときに生じるファクトリカスケードです。カスケードは、total time
とtop-level time
の番号に顕著な違いがあることで識別できます:
total top-level total time time per call top-level time name
208 0 9.5812s 0.0461s 0.0000s namespace
208 76 37.4214s 0.1799s 13.8749s project
上の表では、namespace
オブジェクトを明示的に作成することはありません (top-level == 0
)-これらはすべて暗黙的に作成されます。しかし、それでも208個(各プロジェクトに1個ずつ)あり、9.5秒かかります。
暗黙的な親の関連付けにおいて、名前付きファクトリへのすべての呼び出しに対して単一のオブジェクトを再利用するために、FactoryDefault
を使用することができます:
RSpec.describe API::Search, factory_default: :keep do
let_it_be(:namespace) { create_default(:namespace) }
そうすると、作成するすべてのプロジェクトはこのnamespace
を使用することになり、namespace: namespace
として渡す必要はありません。let_it_be
とともに動作させるには、factory_default: :keep
を明示的に指定する必要があります。これにより、スイート内のすべてのサンプルでデフォルトのファクトリーを保持することができます。
テスト例間の偶発的な依存性を防ぐために、create_default
で作成されたオブジェクトは凍結されます。
208のプロジェクトを作る必要はないのかもしれません。さらに、私たちが作成したプロジェクトのうち、私たちが依頼したプロジェクトは1/3程度(76/208)しかないことがわかります。プロジェクトにもデフォルト値を設定するメリットがあります:
let_it_be(:project) { create_default(:project) }
この場合、total time
とtop-level time
の数字がより一致します:
total top-level total time time per call top-level time name
31 30 4.6378s 0.1496s 4.5366s project
8 8 0.0477s 0.0477s 0.0477s namespace
それではlet
テスト内でオブジェクトを作成して変数に格納するには、 さまざまな方法があります。最も効率の悪いものから最も効率の良いものまであります:
-
let!
は、 例を実行する前にオブジェクトを作成します。また、例ごとに新しいオブジェクトを作成します。このオプションは、明示的にオブジェクトを参照することなく、それぞれの例の前にクリーンなオブジェクトを作成する必要がある場合にのみ使用してください。 -
let
lazilyはオブジェクトを作成します。オブジェクトが呼び出されるまでオブジェクトは作成されません。let
let
単純な値であれば問題let
ありません。let
しかしlet
、ファクトリなどのデータベースモデルを扱う場合は、let
より効率的なlet
-
let_it_be_with_refind
let_it_be_with_reload
はActiveRecord::Base#reload
の代わりにActiveRecord::Base#find
を呼び出します。reload
は通常refind
よりも高速です。 -
let_it_be_with_reload
は同じコンテキストのすべての例に対して一度だけオブジェクトを作成しますが、各例の後にはデータベースの変更がロールバックされ、オブジェクトを元の状態に戻すためにobject.reload
が呼び出されます。これは、例の前や最中にオブジェクトに変更を加えることができることを意味します。しかし、他のモデル間での状態リークが発生する場合もあります。このような場合、特にいくつかの例しか存在しないのであれば、let
の方が簡単かもしれません。 -
let_it_be
は、同じコンテキストのすべての例に対して1回だけオブジェクトを作成します。これは、let
やlet!
の代わりに、例ごとに変更する必要のないオブジェクトを作成するのに適しています。let_it_be
を使用すると、データベースモデルを作成するテストを劇的に高速化することができます。詳細と例についてはhttps://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#let-it-be を参照ください。
プロヒント: テストを書く際には、let_it_be
内部のオブジェクトは不変であると考えたほうがよいでしょう。let_it_be
宣言 let_it_be
内部のオブジェクトを変更する際には、いくつかの重要な注意点があるからですlet_it_be
(1,2) let_it_be
。オブジェクトをイミュータブルにlet_it_be
するには let_it_be
、.freeze
の使用を検討しましょう:
# Before
let_it_be(:namespace) { create_default(:namespace)
# After
let_it_be(:namespace) { create_default(:namespace).freeze
let_it_be
のフリーズについてはhttps://github.com/test-prof/test-prof/blob/master/docs/recipes/let_it_be.md#state-leakage-detection を参照ください。
let_it_be
は、オブジェクトを一度インスタンス化し、そのインスタンスをサンプル間で共有するため、最も最適化されたオプションです。let_it_be
の代わりにlet
が必要になった場合は、let_it_be_with_reload
を試してみてください。
# Old
let(:project) { create(:project) }
# New
let_it_be(:project) { create(:project) }
# If you need to expect changes to the object in the test
let_it_be_with_reload(:project) { create(:project) }
let_it_be
は使用できませんが、let_it_be_with_reload
はlet
よりも効率的です:
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:project) { create(:project) } # The test will fail if `let_it_be` is used
context 'with a developer' do
before do
project.add_developer(user)
end
it 'project has an owner and a developer' do
expect(project.members.map(&:access_level)).to match_array([Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER])
end
end
context 'with a maintainer' do
before do
project.add_maintainer(user)
end
it 'project has an owner and a maintainer' do
expect(project.members.map(&:access_level)).to match_array([Gitlab::Access::OWNER, Gitlab::Access::MAINTAINER])
end
end
ファクトリー内のスタブメソッド
ファクトリー内でallow(object).to receive(:method)
を使用することは避けるべきです。これはlet_it_be
でファクトリーを使用することができなくなるからです。
代わりに、stub_method
を使用してメソッドをスタブすることができます:
before(:create) do |user, evaluator|
# Stub a method.
stub_method(user, :some_method) { 'stubbed!' }
# Or with arguments, including named ones
stub_method(user, :some_method) { |var1| "Returning #{var1}!" }
stub_method(user, :some_method) { |var1: 'default'| "Returning #{var1}!" }
end
# Un-stub the method.
# This may be useful where the stubbed object is created with `let_it_be`
# and you want to reset the method between tests.
after(:create) do |user, evaluator|
restore_original_method(user, :some_method)
# or
restore_original_methods(user)
end
stub_method
をlet_it_be_with_refind
と併用しても動作しません。これは、stub_method
がインスタンスにメソッドをスタブし、stub_method
が実行のたびにオブジェクトの新しいインスタンスを生成するからです。stub_method
はメソッドの存在とメソッドのアリティチェックをサポートしていません。
stub_method
はファクトリーでのみ使用されることになっています。それ以外の場所での使用は強く推奨されません。利用可能であれば、stub_method
RSpecモックのstub_method
使用を検討してください。スタブ・メンバーのアクセス・レベル
Project
やGroup
のようなファクトリースタブのメンバーアクセスレベルをスタブするには、stub_member_access_level
を使用します:
let(:project) { build_stubbed(:project) }
let(:maintainer) { build_stubbed(:user) }
let(:policy) { ProjectPolicy.new(maintainer, project) }
it 'allows admin_project ability' do
stub_member_access_level(project, maintainer: maintainer)
expect(policy).to be_allowed(:admin_project)
end
project_authorizations
またはMember
レコードの永続化に依存している場合は、 このスタブヘルパーの使用を控えてください。代わりにProject#add_member
あるいはGroup#add_member
を使用してください。追加のプロファイリング・メトリクス
rspec_profiling
gem を使用すると、例えばテスト実行時の SQL クエリの数を診断することができます。
これは、テストがトリガーしたアプリケーション側のSQLクエリが、テスト対象外の部分(例えば!123810 )をモックしている可能性があります。
パフォーマンスに関するドキュメントを参照ください。
遅い機能テストのトラブルシューティング
スローフィーチャテストは、一般的に他のテストと同じ方法で最適化することができます。しかし、トラブルシューティングセッションをより実りあるものにするための特別なテクニックがいくつかあります。
機能テストが UI 上で何をしているかを確認します。
# Before
bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
# After
WEBDRIVER_HEADLESS=0 bin/rspec ./spec/features/admin/admin_settings_spec.rb:992
詳しくは :js
spec を可視ブラウザで実行 を参照してください。
プロファイリング使用時のCapybara::DSL#
の検索
stackprof
flamegraphs を使用する場合、検索でCapybara::DSL#
を検索すると、行われたCapybaraアクションとその所要時間を見ることができます!
遅いテストの特定
スペックの最適化を開始するには、プロファイリングを使用してスペックを実行するのがよい方法です。これを行うには
bundle exec rspec --profile -- path/to/spec_file.rb
これには以下のような情報が含まれます:
Top 10 slowest examples (10.69 seconds, 7.7% of total time):
Issue behaves like an editable mentionable creates new cross-reference notes when the mentionable text is edited
1.62 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:164
Issue relative positioning behaves like a class that supports relative positioning .move_nulls_to_end manages to move nulls to the end, stacking if we cannot create enough space
1.39 seconds ./spec/support/shared_examples/models/relative_positioning_shared_examples.rb:88
Issue relative positioning behaves like a class that supports relative positioning .move_nulls_to_start manages to move nulls to the end, stacking if we cannot create enough space
1.27 seconds ./spec/support/shared_examples/models/relative_positioning_shared_examples.rb:180
Issue behaves like an editable mentionable behaves like a mentionable extracts references from its reference property
0.99253 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:69
Issue behaves like an editable mentionable behaves like a mentionable creates cross-reference notes
0.94987 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:101
Issue behaves like an editable mentionable behaves like a mentionable when there are cached markdown fields sends in cached markdown fields when appropriate
0.94148 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:86
Issue behaves like an editable mentionable when there are cached markdown fields when the markdown cache is stale persists the refreshed cache so that it does not have to be refreshed every time
0.92833 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:153
Issue behaves like an editable mentionable when there are cached markdown fields refreshes markdown cache if necessary
0.88153 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:130
Issue behaves like an editable mentionable behaves like a mentionable generates a descriptive back-reference
0.86914 seconds ./spec/support/shared_examples/models/mentionable_shared_examples.rb:65
Issue#related_issues returns only authorized related issues for given user
0.84242 seconds ./spec/models/issue_spec.rb:335
Finished in 2 minutes 19 seconds (files took 1 minute 4.42 seconds to load)
277 examples, 0 failures, 1 pending
この結果から、仕様の中で最もコストの高い例がわかります。ここで最もコストがかかる例は、共有された例です。一般的に、削減された例は、複数の場所で呼び出されるため、より大きな影響を与えます。
最も遅いテスト
rspec_profiling_stats
プロジェクトのテスト時間に関する情報を収集しています。データは GitLab Pages を使ってこのUI に表示されます。
イシューで、テスト期間のしきい値を定義しました。
閾値を満たしていないテストについては、それを改善するために自動的にイシューを作成します。
正当な理由で遅く、イシューの作成をスキップしたいテストには、allowed_to_be_slow: true
を追加します。
日付 | フィーチャーテスト | コントローラとリクエストのテスト | ユニット | その他 | 方法 |
---|---|---|---|---|---|
2023-02-15 | 67.42秒 | 44.66秒 | - | 76.86秒 | トップスローテストで最大 |
2023-06-15 | 50.13秒 | 19.20秒 | 27.12 | 45.40秒 | 低速テストトップ100の平均 |
高価なアクションの繰り返しを回避
孤立した例は非常にわかりやすく、仕様としてのスペックの目的を果たすのに役立ちますが、次の例は、高価なアクションをどのように組み合わせられるかを示しています:
subject { described_class.new(arg_0, arg_1) }
it 'creates an event' do
expect { subject.execute }.to change(Event, :count).by(1)
end
it 'sets the frobulance' do
expect { subject.execute }.to change { arg_0.reset.frobulance }.to('wibble')
end
it 'schedules a background job' do
expect(BackgroundJob).to receive(:perform_async)
subject.execute
end
subject.execute
への呼び出しが高価である場合、異なるアサーションを行うためだけに同じアクションを繰り返すことになります。例を組み合わせることで、この繰り返しを減らすことができます:
it 'performs the expected side-effects' do
expect(BackgroundJob).to receive(:perform_async)
expect { subject.execute }
.to change(Event, :count).by(1)
.and change { arg_0.frobulance }.to('wibble')
end
パフォーマンスを上げるために、明快さとテストの独立性を犠牲にすることになるので、注意してください。
テストを組み合わせる場合は、最初の失敗だけでなく、完全な結果が得られるように:aggregate_failures
を使用することを検討してください。
万が一
私たちはbackend_testing_performance
ドメインの専門知識を持っており、遅いバックエンドの仕様のリファクタリングを手伝ってくれる人をリストアップしています。
手伝ってくれそうな人を探すには、エンジニアリングプロジェクトのページで backend testing performance
を検索するか、 www-gitlab-org
プロジェクトを直接探してください。
フィーチャーカテゴリーのメタデータ
各 RSpec サンプルにフィーチャー・カテゴリーのメタデータを設定する必要があります。
EEライセンスに依存するテスト
コンテキスト/スペック・ブロックでif: Gitlab.ee?
またはunless: Gitlab.ee?
を使用すると、FOSS_ONLY=1
を使用しているかどうかに応じてテストを実行できます。
例SchemaValidator は、ライセンスに応じて異なるパスを読み込みます。
SaaSに依存するテスト
:saas
RSpec メタデータタグヘルパーを context/spec ブロックで使うと、GitLab.com でのみ実行されるコードをテストすることができます。このヘルパーはGitlab.config.gitlab['url']
をGitlab::Saas.com_url
に設定します。
カバレッジ
simplecov
はコードのテストカバレッジレポートを生成するために使われます。これらは CI 上で自動的に生成されますが、ローカルでテストを実行するときには生成されません。自分のマシンで spec ファイルを実行したときに部分的なレポートを生成するには、SIMPLECOV
環境変数を設定します:
SIMPLECOV=1 bundle exec rspec spec/models/repository_spec.rb
カバレッジレポートはアプリルートのcoverage
フォルダに生成され、ブラウザなどで開くことができます:
firefox coverage/index.html
カバレッジ・レポートを使用して、テストがコードの 100% をカバーしていることを確認してください。
システム/機能テスト
- フィーチャー・スペックは、
user_changes_password_spec.rb
のように、ROLE_ACTION_spec.rb
という名前にすべきです。 - シナリオのタイトルは、成功経路と失敗経路を表すものを使用してください。
- 成功した」というような、何の情報も加えないシナリオタイトルは避けてください。
- 特集タイトルを繰り返すシナリオタイトルは避けてください。
- 必要なレコードだけをデータベースに作成
- ハッピーパスとそうでないパスのテスト。
- 他のすべての可能な経路は、ユニットテストまたはインテグレーションテストでテストされるべきです。
- ActiveRecordモデルの内部ではなく、ページに表示されるものをテストしてください。例えば、レコードが作成されたことを確認したいのであれば、
Model.count
が 1 つ増えることではなく、その属性がページに表示されることを期待します。 - DOM 要素を探すのはかまいませんが、乱用は禁物です。
UI テスト
UIをテストするときは、ユーザーが何を見るか、どのようにUIとやりとりするかをシミュレートするテストを書いてください。これは、Capybaraのセマンティックメソッドを優先し、IDやクラス、属性によるクエリを避けることを意味します。
この方法でテストする利点は以下の通りです:
- すべてのインタラクティブな要素がアクセス可能な名前を持っていることを保証します。
- より自然な言葉を使うので、より読みやすくなります。
- ユーザーからは見えないIDやクラス、属性によるクエリを避けることができるので、よりもろくありません。
IDやクラス名、data-testid
でクエリするのではなく、要素のテキスト・ラベルでクエリすることを強くお勧めします。
必要であれば、within
を使うことで、ページの特定の領域内のインタラクションをスコープすることができます。通常はラベルを持たないdiv
のような要素にスコープする可能性が高いので、この場合はdata-testid
セレクタを使うことができます。
be_axe_clean
matcher を使って、axe の自動アクセシビリティテストを機能テストで実行できます。
外部化されたコンテンツ
RSpec のテストでは、外部化されたコンテンツに対する期待値は、 翻訳に合わせて同じ外部化メソッドを呼び出す必要があります。たとえば、Ruby では_
メソッドを使用します。
詳しくはGitLab の国際化 - テストファイル (RSpec)をご覧ください。
アクション
可能であれば、以下のような具体的なアクションを使用してください。
# good
click_button _('Submit review')
click_link _('UI testing docs')
fill_in _('Search projects'), with: 'gitlab' # fill in text input with text
select _('Updated date'), from: 'Sort by' # select an option from a select input
check _('Checkbox label')
uncheck _('Checkbox label')
choose _('Radio input label')
attach_file(_('Attach a file'), '/path/to/file.png')
# bad - interactive elements must have accessible names, so
# we should be able to use one of the specific actions above
find('.group-name', text: group.name).click
find('.js-show-diff-settings').click
find('[data-testid="submit-review"]').click
find('input[type="checkbox"]').click
find('.search').native.send_keys('gitlab')
ファインダー
可能であれば、以下のような、より具体的なファインダーを使用してください。
# good
find_button _('Submit review')
find_button _('Submit review'), disabled: true
find_link _('UI testing docs')
find_link _('UI testing docs'), href: docs_url
find_field _('Search projects')
find_field _('Search projects'), with: 'gitlab' # find the input field with text
find_field _('Search projects'), disabled: true
find_field _('Checkbox label'), checked: true
find_field _('Checkbox label'), unchecked: true
# acceptable when finding a element that is not a button, link, or field
find('[data-testid="element"]')
マッチャー
可能であれば、以下のような、より具体的なマッチャーを使用してください。
# good
expect(page).to have_button _('Submit review')
expect(page).to have_button _('Submit review'), disabled: true
expect(page).to have_button _('Notifications'), class: 'is-checked' # assert the "Notifications" GlToggle is checked
expect(page).to have_link _('UI testing docs')
expect(page).to have_link _('UI testing docs'), href: docs_url # assert the link has an href
expect(page).to have_field _('Search projects')
expect(page).to have_field _('Search projects'), disabled: true
expect(page).to have_field _('Search projects'), with: 'gitlab' # assert the input field has text
expect(page).to have_checked_field _('Checkbox label')
expect(page).to have_unchecked_field _('Radio input label')
expect(page).to have_select _('Sort by')
expect(page).to have_select _('Sort by'), selected: 'Updated date' # assert the option is selected
expect(page).to have_select _('Sort by'), options: ['Updated date', 'Created date', 'Due date'] # assert an exact list of options
expect(page).to have_select _('Sort by'), with_options: ['Created date', 'Due date'] # assert a partial list of options
expect(page).to have_text _('Some paragraph text.')
expect(page).to have_text _('Some paragraph text.'), exact: true # assert exact match
expect(page).to have_current_path 'gitlab/gitlab-test/-/issues'
expect(page).to have_title _('Not Found')
# acceptable when a more specific matcher above is not possible
expect(page).to have_css 'h2', text: 'Issue title'
expect(page).to have_css 'p', text: 'Issue description', exact: true
expect(page).to have_css '[data-testid="weight"]', text: 2
expect(page).to have_css '.atwho-view ul', visible: true
モーダルとの対話
GitLab UI のモーダルとやりとりするにはwithin_modal
ヘルパーを使います。
include Spec::Support::Helpers::ModalHelpers
within_modal do
expect(page).to have_link _('UI testing docs')
fill_in _('Search projects'), with: 'gitlab'
click_button 'Continue'
end
さらに、accept_gl_confirm
を、受諾だけが必要な確認モダルに使うこともできます。これはwindow.confirm()
をconfirmAction
にマイグレーションするときに便利です。
include Spec::Support::Helpers::ModalHelpers
accept_gl_confirm do
click_button 'Delete user'
end
また、期待される確認メッセージとボタンテキストをaccept_gl_confirm
に渡すこともできます。
include Spec::Support::Helpers::ModalHelpers
accept_gl_confirm('Are you sure you want to delete this user?', button_text: 'Delete') do
click_button 'Delete user'
end
その他の便利なメソッド
finderメソッドを使って要素を取得した後、hover
のような多くの要素メソッドを呼び出すことができます。
Capybara テストでは、accept_confirm
のようなセッションメソッドも利用できます。
その他の便利なメソッドを以下に示します:
refresh # refresh the page
send_keys([:shift, 'i']) # press Shift+I keys to go to the Issues dashboard page
current_window.resize_to(1000, 1000) # resize the window
scroll_to(find_field('Comment')) # scroll to an element
また、spec/support/helpers/
ディレクトリには GitLab カスタムヘルパーがたくさんあります。
ライブデバッグ
ブラウザの動作を見てCapybaraテストをデバッグする必要がある場合があります。
Capybaraを一時停止し、specのlive_debug
。現在のページは自動的にデフォルトブラウザで開かれます。最初にサインインする必要があるかもしれません (現在のユーザーの認証情報がターミナルに表示されます)。
テスト実行を再開するには、いずれかのキーを押します。
使用例:
$ bin/rspec spec/features/auto_deploy_spec.rb:34
Running via Spring preloader in process 8999
Run options: include {:locations=>{"./spec/features/auto_deploy_spec.rb"=>[34]}}
Current example is paused for live debugging
The current user credentials are: user2 / 12345678
Press any key to resume the execution of the example!
Back to the example!
.
Finished in 34.51 seconds (files took 0.76702 seconds to load)
1 example, 0 failures
live_debug
JavaScriptが有効なスペックでのみ動作します。
:js
spec を表示可能なブラウザで実行します。
このように、WEBDRIVER_HEADLESS=0
で spec を実行してください:
WEBDRIVER_HEADLESS=0 bin/rspec some_spec.rb
テストはすぐに完了しますが、これで何が起きているかがわかります。WEBDRIVER_HEADLESS=0
でlive_debug
を使うと、開いているブラウザを一時停止し、ページを再び開きません。これは、デバッグや要素の検査に使用できます。
また、byebug
やbinding.pry
を追加することで、実行を一時停止し、テストを段階的に進めることができます。
スクリーンショット
失敗時に自動的にスクリーンショットを撮るためにcapybara-screenshot
gem を使っています。CIでは、これらのファイルをジョブのアーティファクトとしてダウンロードできます。
また、以下のメソッドを追加することで、テストの任意のタイミングで手動でスクリーンショットを撮ることもできます。不要になったら必ず削除してください!詳しくはhttps://github.com/mattheworiordan/capybara-screenshot#manual-screenshots を参照してください。
Capybaraが “見ている “ものをスクリーンショットするために、:js
specにscreenshot_and_save_page
を追加し、ページソースを保存してください。
:js
specにscreenshot_and_open_image
を追加して、Capybaraが「見た」ものをスクリーンショットし、自動的に画像を開きます。
これによって作成されたHTMLダンプにはCSSがありません。その結果、実際のアプリケーションとはまったく異なる見た目になってしまいます。デバッグを簡単にするCSSを追加する小さなハックがあります。
高速なユニットテスト
一部のクラスはRailsからうまく分離されています。Rails環境やBundlerの:default
グループのgemロードによるオーバーヘッドなしにテストできるはずです。このような場合、テストファイルでrequire 'spec_helper'
の代わりにrequire 'fast_spec_helper'
を使用すると、テストが非常に高速に実行されます:
- gemのロードがスキップされます。
- Railsアプリの起動がスキップされます。
- GitLab ShellとGitalyのセットアップはスキップされます。
- テストリポジトリのセットアップはスキップされます。
fast_spec_helper
lib/
ディレクトリ lib/
内にあるクラスの自動ロードもサポートします。lib/
クラスやモジュールが lib/
ディレクトリlib/
のコードのみを使用 lib/
fast_spec_helper
するfast_spec_helper
場合、依存関係を明示的にロードする必要はありません。 fast_spec_helper
また、Rails環境でよく使用されるCore拡張を含む、すべてのActiveSupport拡張もロードします。
gemsを使用しているコードや、依存関係がlib/
にない場合は、require_dependency
を使用して依存関係をロードする必要がある場合があります。
たとえば、re2
ライブラリを内部で使用しているGitlab::UntrustedRegexp
クラスを呼び出しているコードをテストしたい場合、次のいずれかを行う必要があります:
-
re2
gemを必要とするライブラリのファイルにrequire_dependency 're2'
。この方法が望ましいです。 - 仕様そのものに追加してください。
- RuboCop 関連の仕様には
rubocop_spec_helper
を使用してください。
通常のspec_helper
を使用した場合、テストのロードに 30 秒以上かかるのに対し、fast_spec_helper
を使用した場合、ロードに約 1 秒かかります。
bin/rspec
を使って個別に仕様を実行してください。spec_helper
を自動的に読み込むので、bin/spring rspec
は使わないでください。
subject
とlet
変数
GitLab RSpecスイートでは、重複を減らすためにlet
(そして、その厳密で遅延のないバージョンlet!
)変数を多用してきました。しかし、これは時にわかりやすさを犠牲にすることになるので、今後の使い方のガイドラインを決める必要があります:
-
let!
変数はインスタンス変数よりもlet
変数の方がlet!
変数よりも望ましいです。let
変数よりも、内部変数の方が望ましいです。 - 仕様ファイル全体の重複を減らすには、
let
を使用してください。 - ひとつのテストで使用する変数の定義には
let
を使用せず、テストのit
ブロック内部でローカル変数として定義します。 - トップレベルの
describe
ブロックの内部でlet
変数を定義し、その変数がcontext
やdescribe
ブロックの内部で使用されるようにしないでください。定義する場所は、使用する場所にできるだけ近づけてください。 -
let
変数の定義を別の変数で上書きすることは避けてください。 -
let
、別の変数の定義でしか使われないような変数は定義しないでください。代わりにヘルパーメソッドを使用してください。 -
let!
変数は、定義された順序で厳密に評価する必要がある場合にのみ使用しますlet
。let
変数が参照されるまで評価されないlet
ことをlet
忘れないでください。 - 例では
subject
を参照しないようにしてください。subject(:name)
、またはlet
変数を使用し、変数が文脈に応じた名前を持つようにしてください。 - もし例の中でtheが参照されることがないのであれば、名前を付けずに
subject
定義してもかまいませんsubject
。
一般的なテストの設定
場合によっては、テスト用の同じオブジェクトを例ごとに再作成する必要はありません。たとえば、プロジェクトとそのプロジェクトのゲストが同じプロジェクトのイシューをテストするために必要なので、1つのプロジェクトとユーザーでファイル全体に対して十分です。
できる限り、before(:all)
やbefore(:context)
を使用して実装しないでください。 もし実装する場合、これらのフックはデータベーストランザクションの外部で実行されるため、手動でデータをクリーンアップする必要があります。
代わりに、let_it_be
変数とtest-prof
gemのbefore_all
フックを使用することで実現できます。
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
before_all do
project.add_guest(user)
end
この結果、このコンテキストで作成されるのはProject
、User
、ProjectMember
の3つだけとなります。
let_it_be
とbefore_all
は入れ子になったコンテキストでも使用できます。コンテキストの後始末は、トランザクションのロールバックを使用して自動的に処理されます。
let_it_be
ブロック内部で定義されたオブジェクトを変更する場合は、次のいずれかを実行する必要があることに注意してください:
- 必要に応じてオブジェクトをリロードします。
-
let_it_be_with_reload
のエイリアスを使用してください。 - 例ごとにリロードするには、
reload
オプションを指定します。
let_it_be_with_reload(:project) { create(:project) }
let_it_be(:project, reload: true) { create(:project) }
また、let_it_be_with_refind
エイリアスを使用したり、refind
オプションを指定して新しいオブジェクトを完全にロードすることもできます。
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:project, refind: true) { create(:project) }
let_it_be
はallow
のようなスタブを持つファクトリでは使用できないことに注意してください。let_it_be
はbefore(:all)
ブロック before(:all)
内で発生before(:all)
し、RSpec は .NET 内でのスタブを許可 before(:all)
しないからです。詳細はこのイシューを参照してください。解決するには、let
を使用するか、ファクトリーを変更してスタブを使用しないようにします。
時間依存のテスト
ActiveSupport::Testing::TimeHelpers
は、時間に敏感なものを検証するために使うことができます。一過性のテスト失敗を防ぐために、時間的な制約があるものを実行したり検証したりするテストでは、これらのヘルパーを使用する必要があります。
使用例:
it 'is overdue' do
issue = build(:issue, due_date: Date.tomorrow)
travel_to(3.days.from_now) do
expect(issue).to be_overdue
end
end
RSpec ヘルパー
:freeze_time
および:time_travel_to
RSpec メタデータタグヘルパーを使用すると、ActiveSupport::Testing::TimeHelpers
メソッドで仕様全体をラップするのに必要な定型コードを減らすことができます。
describe 'specs which require time to be frozen', :freeze_time do
it 'freezes time' do
right_now = Time.now
expect(Time.now).to eq(right_now)
end
end
describe 'specs which require time to be frozen to a specific date and/or time', time_travel_to: '2020-02-02 10:30:45 -0700' do
it 'freezes time to the specified date and time' do
expect(Time.now).to eq(Time.new(2020, 2, 2, 17, 30, 45, '+00:00'))
end
end
内部では、これらのヘルパーはaround(:each)
フックとActiveSupport::Testing::TimeHelpers
メソッドのブロック構文を使用します:
around(:each) do |example|
freeze_time { example.run }
end
around(:each) do |example|
travel_to(date_or_time) { example.run }
end
例を実行する前に作成されたオブジェクト(let_it_be
を介して作成されたオブジェクトなど)は、spec のスコープ外になることを覚えておいてください。すべての時間を凍結する必要がある場合は、before :all
を使用してセットアップをカプセル化することもできます。
before :all do
freeze_time
end
after :all do
unfreeze_time
end
タイムスタンプの切り捨て
Active Recordのタイムスタンプは、RailsのActiveRecord::Timestamp
](https://github.com/rails/rails/blob/1eb5cc13a2ed8922b47df4ae47faf5f23faf3d35/activerecord/lib/active_record/timestamp.rb#L105) モジュールがTime.now
を使って[設定します。時間の精度はOSに依存し、ドキュメントにあるように秒未満の端数が含まれることがあります。
Railsモデルがデータベースに保存されるとき、そのモデルが持つタイムスタンプはPostgreSQLのtimestamp without time zone
。そのため、1577987974.6472975
がPostgreSQLに送信されると、小数部の最後の桁が切り捨てられ、代わりに1577987974.647297
が保存されます。
この結果は以下のような簡単なテストになります:
let_it_be(:contact) { create(:contact) }
data = Gitlab::HookData::IssueBuilder.new(issue).build
expect(data).to include('customer_relations_contacts' => [contact.hook_attrs])
のようなエラーで失敗します:
expected {
"assignee_id" => nil, "...1 +0000 } to include {"customer_relations_contacts" => [{:created_at => "2023-08-04T13:30:20Z", :first_name => "Sidney Jones3" }]}
Diff:
@@ -1,35 +1,69 @@
-"customer_relations_contacts" => [{:created_at=>"2023-08-04T13:30:20Z", :first_name=>"Sidney Jones3" }],
+"customer_relations_contacts" => [{"created_at"=>2023-08-04 13:30:20.245964000 +0000, "first_name"=>"Sidney Jones3" }],
正しい精度でタイムスタンプを取得するために、データベースから.reload
:
let_it_be(:contact) { create(:contact) }
data = Gitlab::HookData::IssueBuilder.new(issue).build
expect(data).to include('customer_relations_contacts' => [contact.reload.hook_attrs])
この説明は Maciek Rząsa のブログ記事から引用しました。
この問題が発生したマージリクエストと、この問題が議論されたバックエンドペアリングセッションを見ることができます。
テストの機能フラグ
このセクションは機能フラグを使った開発に移動しました。
純粋なテスト環境
一つの GitLab テストで実行されるコードは、多くのデータにアクセスしたり変更したりする可能性があります。テスト実行前の準備やテスト実行後のクリーンアップを入念に行わないと、テストがデータを変更してしまい、後続のテストの挙動に影響を及ぼす可能性があります。これは絶対に避けるべきです!幸いなことに、既存のテストフレームワークはすでにほとんどのケースを処理しています。
テスト環境が汚染された場合、一般的な結果はテストの不具合です。汚染は多くの場合、順序依存として現れます。つまり、仕様 A の後に仕様 B を実行すると確実に失敗しますが、仕様 B の後に仕様 A を実行すると確実に成功します。このような場合、rspec --bisect
(あるいは手動で spec ファイルを二等分する方法) を使って、どの spec に問題があるかを調べることができます。この問題を解決するには、テストスイートがどのように環境を原始的な状態に保つかを理解する必要があります。各データストアの詳細については、続きをお読みください!
SQL データベース
これはdatabase_cleaner
gem によって管理されています。各仕様はトランザクションで囲まれており、テスト完了後にロールバックされます。一部の仕様では、完了後にすべてのテーブルに対してDELETE FROM
クエリを発行します。これにより、作成された行を複数のデータベース接続から表示できるようになります。これは、ブラウザで実行する仕様やマイグレーション仕様などで重要です。
よく知られているTRUNCATE TABLES
アプローチではなく、これらのストラテジーを使用することの結果の 1 つは、主キーやその他のシーケンスが spec 間でリセットされないことです。そのため、仕様 A でプロジェクトを作成し、仕様 B でプロジェクトを作成した場合、最初のプロジェクトはid=1
になり、2 番目のプロジェクトはid=2
になります。
つまり、ID やその他のシーケンスで生成されたカラムの値に依存してはなりません。偶発的なコンフリクトを避けるため、この種のカラムの値を手動で指定することも避けるべきです。その代わりに、これらの列の値は未指定のままにしておき、行が作成された後に値を調べます。
マイグレーション仕様における TestProf
上述したように、マイグレーション仕様はデータベーストランザクション内で実行することはできません。私たちのテストスイートではTestProfを使用してテストスイートの実行時間を改善していますが、TestProf
データベーストランザクションを使用してこれらの最適化を TestProf
行います。TestProf
この TestProf
ため、マイグレーション仕様ではメソッドをTestProf
使用 TestProf
できません。これらは使用してはいけないメソッドで、代わりにデフォルトの RSpec メソッドに置き換える必要があります:
-
let_it_be
代わりにlet
またはlet!
を使用してください。 -
let_it_be_with_reload
代わりにlet
またはlet!
を使用してください。 -
let_it_be_with_refind
代わりにlet
またはlet!
を使用してください。 -
before_all
代わりにbefore
またはbefore(:all)
を使用してください。
Redis
GitLabはRedisにキャッシュされたアイテムとSidekiqジョブの2つのデータを保存します。別個のRedisインスタンスによってバックアップされているGitlab::Redis::Wrapper
の子孫 の全リストをご覧ください。
ほとんどの仕様では、Railsキャッシュは実際にはインメモリストアです。これは仕様間で置き換えられるため、Rails.cache.read
とRails.cache.write
への呼び出しは安全です。ただし、specがRedisを直接呼び出す場合は、適切な:clean_gitlab_redis_cache
、:clean_gitlab_redis_shared_state
、:clean_gitlab_redis_queues
。
バックグラウンドジョブ / Sidekiq
デフォルトでは、Sidekiqジョブはジョブ配列にキューイングされ、処理されません。テストがSidekiqジョブをキューに入れ、それらを処理する必要がある場合、:sidekiq_inline
特性を使用できます。
この:sidekiq_might_not_need_inline
特性は :sidekiq_might_not_need_inline
、:sidekiq_might_not_need_inline
Sidekiqのインラインモードがフェイクモードに変更 :sidekiq_might_not_need_inline
された:sidekiq_might_not_need_inline
ときに、実際にジョブを処理するためにSidekiqを必要とするすべてのテストに追加さ :sidekiq_might_not_need_inline
れました。:sidekiq_might_not_need_inline
この特性を持つテストは、Sidekiqのジョブ処理に依存しないように修正さ :sidekiq_might_not_need_inline
れるか、バックグラウンドジョブの処理が必要/期待される場合は、:sidekiq_might_not_need_inline
その :sidekiq_might_not_need_inline
特性を:sidekiq_inline
。
私たちのSidekiqワーカーはApplicationJob
/ActiveJob::Base
を継承していないため、perform_enqueued_jobs
の使用は遅延メール配信のテストにのみ役立ちます。
DNS
DNS は開発者のローカルネットワークによってイシューが発生する可能性があるため、 テストスイートでは DNS リクエストをスタブしています (!22368 現在)。spec/support/dns.rb
には RSpec ラベルが用意されており、 DNS スタブを回避したい場合にテストに適用することができます:
it "really connects to Prometheus", :permit_dns do
また、より具体的な制御が必要な場合は、DNS ブロッキングはspec/support/helpers/dns_helpers.rb
で実装されており、これらのメソッドを別の場所で呼び出すことができます。
速度制限
テスト・スイートではレート制限が有効になっています。レート制限は、:js
特性を使用する機能仕様でトリガーされる可能性があります。ほとんどの場合、:clean_gitlab_redis_rate_limiting
特性を spec にマークすることで、レート制限のトリガーを回避できます。この特性は、spec 間で Redis キャッシュに保存されたレート制限データをクリアします。単一のテストでレート制限がトリガされる場合は、:disable_rate_limit
を代わりに使用できます。
ファイルメソッドのスタブ
File.read
などのメソッドをスタブする必要がある場合は、必ず以下のようにしてください:
-
File.read
を、関心のあるファイル・パスのみにスタブしてください。 - 他のファイルパスについては、元の実装を呼び出してください。
そうしないと、コードベースの他の部分からのFile.read
呼び出しが正しくスタブされません。File.read
のスタブ処理を正しく行うstub_file_read
, およびexpect_file_read
ヘルパーメソッドを使用する必要があります。
# bad, all Files will read and return nothing
allow(File).to receive(:read)
# good
stub_file_read(my_filepath)
# also OK
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(my_filepath)
ファイルシステム
ファイルシステムのデータは「リポジトリ」と「それ以外」に大別できます。リポジトリはtmp/tests/repositories
に保存されます。このディレクトリは、テスト実行の開始前と終了後に空になります。スペック間は空にならないため、作成されたリポジトリはプロセスの有効期間中、このディレクトリに蓄積されます。削除にはコストがかかりますが、注意深く管理しないと汚染につながる可能性があります。
これを避けるために、テスト・スイートではハッシュ化されたストレージが有効になっています。つまり、リポジトリにはプロジェクト ID に依存する一意のパスが与えられます。プロジェクト ID はスペック間でリセットされないため、各スペックはディスク上に独自のリポジトリを取得し、スペック間で変更が見えないようにします。
スペックが手動でプロジェクト ID を指定したり、tmp/tests/repositories/
ディレクトリの状態を直接検査したりする場合は、実行前と実行後の両方でディレクトリをクリーンアップする必要があります。一般的に、これらのパターンは完全に避けるべきです。
アップロードなど、データベースオブジェクトにリンクされた他のクラスのファイルも、一般的に同じ方法で管理されます。仕様でハッシュストレージが有効になっている場合、それらはIDによって決定された場所にディスクに書き込まれるため、競合は発生しないはずです。
projects
ファクトリに:legacy_storage
特性を渡すことで、ハッシュストレージを無効にする仕様もあります。このような仕様では、プロジェクトやそのグループのpath
を決してオーバーライドしてはなりません。デフォルトのパスにはプロジェクト ID が含まれているため、競合することはありません。2つの仕様が同じパスで:legacy_storage
プロジェクトを作成すると、ディスク上の同じリポジトリを使用することになり、テスト環境の汚染につながります。
その他のファイルは、spec が手動で管理する必要があります。たとえば、tmp/test-file.csv
ファイルを作成するコードを実行する場合、spec はクリーンアップの一環としてそのファイルが削除されるようにしなければなりません。
永続的なメモリ内アプリケーションの状態
あるrspec
の実行に含まれるすべての spec は同じ Ruby プロセスを共有しており、spec 間でアクセス可能な Ruby オブジェクトを変更することで、互いに影響を与え合うことができます。実際には、これはグローバル変数や定数(Ruby のクラスやモジュールなどを含む)を意味します。
グローバル変数は通常、変更すべきではありません。どうしても必要な場合は、このようなブロックを使って変更後に確実にロールバックすることができます:
around(:each) do |example|
old_value = $0
begin
$0 = "new-value"
example.run
ensure
$0 = old_value
end
end
定数を変更する必要がある場合は、stub_const
ヘルパーを使用して、変更がロールバックされるようにします。
ENV
定数の内容を変更する必要がある場合は、代わりにstub_env
ヘルパーメソッドを使用します。
ほとんどのRubyインスタンスは仕様間で共有されませんが、クラスと モジュールは一般的に共有されます。クラスやモジュールのインスタンス変数、アクセサ、クラス変数、その他のステートフルなイディオムは、グローバル変数と同じように扱うべきです。必要でない限り、変更しないでください!特に、期待値や依存性注入とスタブを併用することで、修正の必要性を避けることができます。他に選択肢がない場合は、グローバル変数の例のようなaround
ブロックを使用することもできますが、可能な限り避けてください。
Elasticsearch の仕様
GitLab 14.0 で導入されました。
Elasticsearch を必要とする仕様には:elastic
trait を付ける必要があります。これは全ての例の前後にインデックスを作成・削除します。
:elastic_delete_by_query
特性は、各コンテキストの開始時と終了時のみにインデックスを作成・削除することで、パイプラインの実行時間を短縮するために追加されました。Elasticsearch delete by query APIは、クリーンなインデックスを確保するために、クエリ間の全てのインデックスのデータを削除するために使用されます。
:elastic_clean
特性は、クリーンなインデックスを保証するために、サンプル間でインデックスを作成・削除します。こうすることで、テストが不要なデータで汚染されることがなくなります。:elastic
または:elastic_delete_by_query
特性を使用することでイシューが発生する場合は、:elastic_clean
代わりに :elastic_clean
使用してください:elastic_clean
。 :elastic_clean
Elasticsearch のロジックに関連するテストのほとんどは、以下のようなものです:
- PostgreSQL でデータを作成し、Elasticsearch でインデックスが作成されるのを待ちます。
- そのデータを検索します。
- テストが期待通りの結果をもたらすことを確認します。
インデックス内の個々のレコードではなく、構造的な変更をチェックするような例外もあります。
Gitlab::Redis::SharedState
.そのため、Elasticsearch の trait は動的にこの:clean_gitlab_redis_shared_state
trait :clean_gitlab_redis_shared_state
を使用します。手動で:clean_gitlab_redis_shared_state
追加する必要は :clean_gitlab_redis_shared_state
ありません。Elasticsearch を使用する仕様では、以下のことが必要です:
- PostgreSQL でデータを作成し、それを Elasticsearch にインデックスします。
- Elasticsearch のアプリケーション設定を有効にします(デフォルトでは無効になっています)。
これを行うには、以下を使用します:
before do
stub_ee_application_setting(elasticsearch_search: true, elasticsearch_indexing: true)
end
さらに、ensure_elasticsearch_index!
メソッドを使用することで Elasticsearch の非同期性を克服することができます。このメソッドはElasticsearch Refresh APIを使用し、前回のリフレッシュ以降にインデックスに対して行われたすべてのオペレーションが検索に利用可能であることを確認します。このメソッドは通常 PostgreSQL にデータをロードした後に呼び出され、データがインデックス化され検索可能になったことを確認します。
Snowplow イベントのテスト
Gitlab::Tracking
をモックするときに例外をキャッチするのが難しくなります。型チェックによる実行時エラーをキャッチするには、Gitlab::Tracking#event
への呼び出しをチェックするexpect_snowplow_event
を使うことができます。
describe '#show' do
it 'tracks snowplow events' do
get :show
expect_snowplow_event(
category: 'Experiment',
action: 'start',
namespace: group,
project: project
)
expect_snowplow_event(
category: 'Experiment',
action: 'sent',
property: 'property',
label: 'label',
namespace: group,
project: project
)
end
end
イベントが呼び出されなかったことを確認したい場合は、expect_no_snowplow_event
を使用できます。
describe '#show' do
it 'does not track any snowplow events' do
get :show
expect_no_snowplow_event(category: described_class.name, action: 'some_action')
end
end
category
とaction
は省略可能ですが、テストの不具合を避けるために、少なくともcategory
を指定する必要があります。例えば、Users::ActivityService
は API リクエストの後に Snowplow イベントを追跡する可能性があり、expect_no_snowplow_event
は引数が指定されていないときに実行されると失敗します。
スキーマに対する Snowplow コンテキストのテスト
Snowplow schema matcherは、JSON スキーマに対して Snowplow コンテキストをテストすることで、検証エラーを減らすのに役立ちます。schema matcher は以下のパラメータを受け付けます:
schema path
context
スキーママッチャの仕様を追加するには、次のようにします:
- 新しいスキーマをIgluリポジトリに追加し、同じスキーマを
spec/fixtures/product_intelligence/
ディレクトリにコピーします。 - コピーしたスキーマで、
"$schema"
キーと値を削除してください。このキーはspecには必要ありませんし、キーが残っているとURLからスキーマを探そうとしてspecが失敗します。 -
次のスニペットでスキーマ・マッチャーを呼び出してください:
match_snowplow_context_schema(schema_path: '<filename from step 1>', context: <Context Hash> )
テーブルベース/パラメータ化テスト
このスタイルのテストは、ひとつのコードに対してさまざまな入力を行うものです。テストケースを一度だけ指定し、入力の表とそれぞれの期待される出力を並べることで、 テストを読みやすくコンパクトにすることができます。
ここでは、RSpec::Parameterizedgemを使用します。テーブル構文を使用し、Rubyの等値性をチェックする簡単な例は次のようになります:
describe "#==" do
using RSpec::Parameterized::TableSyntax
let(:one) { 1 }
let(:two) { 2 }
where(:a, :b, :result) do
1 | 1 | true
1 | 2 | false
true | true | true
true | false | false
ref(:one) | ref(:one) | true # let variables must be referenced using `ref`
ref(:one) | ref(:two) | false
end
with_them do
it { expect(a == b).to eq(result) }
it 'is isomorphic' do
expect(b == a).to eq(result)
end
end
end
テーブルベースのテストを作成した後で、次のようなエラーが表示された場合:
NoMethodError:
undefined method `to_params'
param_sets = extracted.is_a?(Array) ? extracted : extracted.to_params
^^^^^^^^^^
Did you mean? to_param
これは、using RSpec::Parameterized::TableSyntax
という行を spec ファイルに含める必要があることを示しています。
where
ブロックの入力には、単純な値のみを使用してください。プロック、ステートフル オブジェクト、FactoryBot が作成したオブジェクトなどを使用すると、予期しない結果になることがあります。Prometheus テスト
Prometheusのメトリクスは、テストの実行ごとに保持されます。例ごとにメトリクスがリセットされるようにするには、RSpec テストに:prometheus
タグを追加します。
マッチャー
カスタム・マッチャーは、RSpecの期待値の意図を明確にしたり、複雑さを隠したりするために作成する必要があります。これらはspec/support/matchers/
の下に置くべきです。マッチャーは、特定の種類の仕様 (機能やリクエストなど) にのみ適用される場合はサブフォルダに置くことができますが、複数の種類の仕様に適用される場合は置くべきではありません。
be_like_time
データベースから返される時刻は、Ruby の時刻オブジェクトとは精度が異なることがあります。
PostgreSQLのtime型とtimestamp型の分解能は1マイクロ秒です。しかし、RubyTime
の精度はOSによって異なることがあります。
次のスニペットを見てください:
project = create(:project)
expect(project.created_at).to eq(Project.find(project.id).created_at)
Linux では、Time
は最大精度 9 を持つことができ、project.created_at
は同じ精度の値(2023-04-28 05:53:30.808033064
のようなもの)を持っています。しかし、データベースに保存され、データベースから読み込まれた実際の値created_at
(2023-04-28 05:53:30.808033
のような)は同じ精度を持っておらず、マッチは失敗します。MacOS Xでは、Time
の精度はPostgreSQLのタイムスタンプ型の精度と一致し、照合は成功します。
このイシューを回避するために、be_like_time
またはbe_within
を使用して、時刻が互いに1秒以内であることを比較することができます。
使用例:
expect(metrics.merged_at).to be_like_time(time)
be_within
の例:
expect(violation.reload.merged_at).to be_within(0.00001.seconds).of(merge_request.merged_at)
have_gitlab_http_status
have_http_status
やexpect(response.status).to
よりもhave_gitlab_http_status
のほうがいいでしょう。 なぜなら、前者はステータスが不一致のときにレスポンスボディを表示できるからです。これは、あるテストが壊れ始めたときに、ソースを編集してテストを再実行することなく、その原因を知りたい場合に非常に便利です。
特に、500 の内部サーバーエラーが表示されたときに便利です。
数値表現の206
よりも、:no_content
のような名前付きの HTTP ステータスを優先してください。サポートされているステータスコードの一覧を参照してください。
使用例:
expect(response).to have_gitlab_http_status(:ok)
match_schema
と match_response_schema
match_schema
matcher を使用すると、サブジェクトがJSON スキーマに一致するかどうかを検証できます。expect
内部の項目は、JSON 文字列または JSON 互換のデータ構造です。
match_response_schema
は、レスポンスオブジェクトで使用するための便宜的な matcher です。
例:
# Matches against spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json
expect(data).to match_schema('prometheus/additional_metrics_query_result')
# Matches against ee/spec/fixtures/api/schemas/board.json
expect(data).to match_schema('board', dir: 'ee')
# Matches against a schema made up of Ruby data structures
expect(data).to match_schema(Atlassian::Schemata.build_info)
be_valid_json
be_valid_json
文字列がJSONとしてパースされ、空でない結果を与えることを検証できます。上記のスキーママッチングと組み合わせるには、and
を使います:
expect(json_string).to be_valid_json
expect(json_string).to be_valid_json.and match_schema(schema)
be_one_of(collection)
include
の逆で、collection
に期待される値が含まれているかどうかをテストします:
expect(:a).to be_one_of(%i[a b c])
expect(:z).not_to be_one_of(%i[a b c])
クエリ性能のテスト
クエリのパフォーマンスをテストすることで、以下のことが可能になります:
- コードのブロックにN+1個の問題が存在しないことを保証します。
- コード・ブロック内のクエリの数が気づかないうちに増えないようにします。
クエリーレコーダー
QueryRecorder
を使用すると、指定したコードブロック内で実行されたデータベースクエリの数のプロファイリングとテストを行うことができます。
詳細はQueryRecorder
のセクションを参照してください。
GitalyClient
Gitlab::GitalyClient.get_request_count
は、与えられたコードのブロックによって行われたGitalyクエリの数をテストすることができます:
詳しくはGitaly Request Counts
。
共有コンテキスト
1つの仕様ファイルにのみ使用される共有コンテキストは、インラインで宣言することができます。複数の仕様ファイルで使用される共有コンテキスト:
-
spec/support/shared_contexts/
の下に置く必要があります。 - 特定の種類の仕様 (機能や要望など) にのみ適用される場合はサブフォルダに入れることができますが、 複数の種類の仕様に適用される場合はサブフォルダに入れるべきではありません。
各ファイルは1つのコンテキストのみを含み、spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb
のような説明的な名前を持つ必要があります。
共有の例
1つの仕様ファイルで使用される共有例は、インラインで宣言することができます。複数の仕様ファイルで使用される共有例:
-
spec/support/shared_examples/
の下に置く必要があります。 - 特定の種類の仕様 (機能や要望など) にのみ適用される場合はサブフォルダに入れることができますが、 複数の種類の仕様に適用される場合はサブフォルダに入れるべきではありません。
各ファイルは1つのコンテキストのみを含み、spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb
のような説明的な名前を持つ必要があります。
ヘルパー
ヘルパーは通常、特定の RSpec の例の複雑さを隠すためのメソッドを提供するモジュールです。他の仕様と共有するつもりがなければ、RSpec ファイルの中でヘルパーを定義することができます。そうでない場合は、spec/support/helpers/
の下に置きます。ヘルパーは、特定の種類の仕様 (機能やリクエストなど) にのみ適用される場合はサブフォルダに置くことができますが、複数の種類の仕様に適用される場合は置くべきではありません。
ヘルパーはRailsの命名規則/namespacing規則(spec/support/helpers/
をルートとする)に従うべきです。インスタンスspec/support/helpers/features/iteration_helpers.rb
:
# frozen_string_literal: true
module Features
module IterationHelpers
def iteration_period(iteration)
"#{iteration.start_date.to_fs(:medium)} - #{iteration.due_date.to_fs(:medium)}"
end
end
end
ヘルパーはRSpecの設定を変更してはいけません。例えば、上記のヘルパーモジュールは以下のように定義します:
# bad
RSpec.configure do |config|
config.include Features::IterationHelpers
end
# good, include in specific spec
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include Features::IterationHelpers
end
Ruby定数のテスト
Ruby定数を使用するコードをテストする際には、定数の値をテストするのではなく、定数に依存する動作にフォーカスしてください。
たとえば、次のようにすると、クラスメソッド.categories
の振る舞いをテストできるので好ましいでしょう。
describe '.categories' do
it 'gets CE unique category names' do
expect(described_class.categories).to include(
'deploy_token_packages',
'user_packages',
# ...
'kubernetes_agent'
)
end
end
一方、定数の値そのものをテストするのは、コードとテストの値を繰り返すだけで、あまり意味がありません。
describe CATEGORIES do
it 'has values' do
expect(CATEGORIES).to eq([
'deploy_token_packages',
'user_packages',
# ...
'kubernetes_agent'
])
end
end
定数のエラーが致命的な影響を及ぼすようなクリティカルなケースでは、定数の値をテストすることは、追加の安全策として有用かもしれません。例えば、GitLabのサービス全体をダウンさせたり、顧客に必要以上の請求をしたり、宇宙を崩壊させたりするような場合です。
ファクトリー
GitLabはテストフィクスチャの代替としてfactory_bot
。
- ファクトリの定義は
spec/factories/
にあり、対応するモデルの複数形を使って命名されます (User
のファクトリはusers.rb
で定義されます )。 - 1つのファイルにトップレベルのファクトリー定義は1つだけあるべきです。
- FactoryBot メソッドはすべての RSpec グループに混在しています。つまり、
FactoryBot.create(...)
の代わりにcreate(...)
を呼び出すことができます。 - traitsを使用して、定義と使用法をクリーンアップします。
- ファクトリーを定義する際には、結果のレコードがバリデーションを通過するために必要でない属性を定義しないようにしましょう。
- ファクトリからインスタンスを作成する際には、 テストで必要とされない属性を指定しないようにしましょう。
-
コールバックでの関連付けの設定には、
create
/build
のかわりに暗黙的、明示的、あるいはインラインの関連付けを使用しましょう。詳細はイシュー#262624を参照ください。has_many
およびbelongs_to
のアソシエーションを持つファクトリを作成するとき、instance
メソッドを使用してビルドされるオブジェクトを参照します。これにより、相互接続されたアソシエーションを使用して不要なレコードが作成されるのを防ぎます。例えば、以下のクラスがあるとします:
class Car < ApplicationRecord has_many :wheels, inverse_of: :car, foreign_key: :car_id end class Wheel < ApplicationRecord belongs_to :car, foreign_key: :car_id, inverse_of: :wheel, optional: false end
次のようなファクトリを作成できます:
FactoryBot.define do factory :car do transient do wheels_count { 2 } end wheels do Array.new(wheels_count) do association(:wheel, car: instance) end end end end FactoryBot.define do factory :wheel do car { association :car } end end
- ファクトリーは
ActiveRecord
オブジェクトに限定する必要はありません。例を見てください。 - ファクトリーとその特性は、仕様によって検証された有効なオブジェクトを生成する必要があります。
- ファクトリーでは
skip_callback
の使用を避けてください。詳しくはイシュー#247865を参照してください。
フィクスチャ
すべての備品はspec/fixtures/
の下に置いてください。
リポジトリ
マージリクエストのようないくつかの機能をテストするには、テスト環境に特定の状態の Git リポジトリが必要です。GitLab は特定の一般的なケースのためにgitlab-test
リポジトリを維持しています。プロジェクトファクトリの:repository
特性でリポジトリのコピーが使用されるようにすることができます:
let(:project) { create(:project, :repository) }
できる限り、:repository
の代わりに:custom_repo
特性を使うことを検討してください。これにより、プロジェクトのリポジトリのmain
ブランチに表示されるファイルを正確に指定できます。例えば
let(:project) do
create(
:project, :custom_repo,
files: {
'README.md' => 'Content here',
'foo/bar/baz.txt' => 'More content here'
}
)
end
これは、デフォルト権限と指定された内容で、2つのファイルを含むリポジトリを作成します。
設定
RSpec設定ファイルは、RSpecの設定を変更するファイルです(RSpec.configure do |config|
ブロックのようなもの)。これらはspec/support/
の下に置く必要があります。
各ファイルは、spec/support/capybara.rb
やspec/support/carrierwave.rb
のように、特定のドメインに関連付ける必要があります。
ヘルパーモジュールが特定の種類の仕様にのみ適用される場合、config.include
の呼び出しに修飾子を追加する必要があります。例えば、spec/support/helpers/cycle_analytics_helpers.rb
が:lib
とtype: :model
のスペックにのみ適用される場合、次のように記述します:
RSpec.configure do |config|
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
end
のみで構成されている設定ファイルの場合は、spec/spec_helper.rb
で直接config.include
追加する config.include
ことができます。
非常に汎用的なヘルパーについては、そのファイルで使われるspec/support/rspec.rb
ファイルに含めることを検討してくださいspec/fast_spec_helper.rb
。 spec/fast_spec_helper.rb
ファイルについてのspec/fast_spec_helper.rb
詳細はFast unit tests spec/fast_spec_helper.rb
を参照してください。
テスト環境のロギング
テスト環境のサービスは、Gitaly、Workhorse、Elasticsearch、Capybaraなど、テスト実行時に自動的に設定・起動されます。CI で実行されたとき、あるいはサービスをインストールする必要があるとき、テスト環境はセットアップ時間に関する情報をログに記録し、次のようなログメッセージを生成します:
==> Setting up Gitaly...
Gitaly set up in 31.459649 seconds...
==> Setting up GitLab Workhorse...
GitLab Workhorse set up in 29.695619 seconds...
fatal: update refs/heads/diff-files-symlink-to-image: invalid <newvalue>: 8cfca84
From https://gitlab.com/gitlab-org/gitlab-test
* [new branch] diff-files-image-to-symlink -> origin/diff-files-image-to-symlink
* [new branch] diff-files-symlink-to-image -> origin/diff-files-symlink-to-image
* [new branch] diff-files-symlink-to-text -> origin/diff-files-symlink-to-text
* [new branch] diff-files-text-to-symlink -> origin/diff-files-text-to-symlink
b80faa8..40232f7 snippet/multiple-files -> origin/snippet/multiple-files
* [new branch] testing/branch-with-#-hash -> origin/testing/branch-with-#-hash
==> Setting up GitLab Elasticsearch Indexer...
GitLab Elasticsearch Indexer set up in 26.514623 seconds...
この情報は、ローカルで実行しているときやアクションを実行する必要がないときには省略されます。これらのメッセージを常に表示したい場合は、次の環境変数を設定してください:
GITLAB_TESTING_LOG_LEVEL=debug