テストのベストプラクティス

テスト設計

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_ofallow_any_instance_of を使わないようにしましょう (Gotchasを参照)。
  • :each はデフォルトなので、フックに指定しないでください。
  • beforeafter のフックでは、: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_doublespyFactoryBot.build(...) よりも速い。
  • FactoryBot.build(...) および.build_stubbed.create よりも高速です。
  • build,build_stubbed,attributes_for,spy,instance_doubleを使用できる場合は、create を使用しないでください。データベースの永続化は遅いです!

ファクトリードクターを使用して、特定のテストでデータベースの永続化が不要なケースを見つけましょう。

ファクトリ最適化の例12

# 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 timetop-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 timetop-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回だけオブジェクトを作成します。これは、letlet! の代わりに、例ごとに変更する必要のないオブジェクトを作成するのに適しています。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_reloadlet よりも効率的です:

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
note
stub_methodlet_it_be_with_refind と併用しても動作しません。これは、stub_method がインスタンスにメソッドをスタブし、stub_method が実行のたびにオブジェクトの新しいインスタンスを生成するからです。

stub_method はメソッドの存在とメソッドのアリティチェックをサポートしていません。

caution
stub_method はファクトリーでのみ使用されることになっています。それ以外の場所での使用は強く推奨されません。利用可能であれば、stub_methodRSpecモックのstub_method使用を検討してください。

スタブ・メンバーのアクセス・レベル

ProjectGroup のようなファクトリースタブのメンバーアクセスレベルをスタブするには、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
note
テストコードが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-1567.42秒44.66秒-76.86秒トップスローテストで最大
2023-06-1550.13秒19.20秒27.1245.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% をカバーしていることを確認してください。

システム/機能テスト

note
新しいシステムテストを書く前に、システムテストを書かないことを検討してください!
  • フィーチャー・スペックは、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=0live_debug を使うと、開いているブラウザを一時停止し、ページを再び開きません。これは、デバッグや要素の検査に使用できます。

また、byebugbinding.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 秒かかります。

caution
コードとその仕様がRailsからきちんと分離されていることを確認するには、bin/rspec を使って個別に仕様を実行してください。spec_helper を自動的に読み込むので、bin/spring rspec は使わないでください。

subjectlet 変数

GitLab RSpecスイートでは、重複を減らすためにlet(そして、その厳密で遅延のないバージョンlet! )変数を多用してきました。しかし、これは時にわかりやすさを犠牲にすることになるので、今後の使い方のガイドラインを決める必要があります:

  • let! 変数はインスタンス変数よりもlet 変数の方がlet! 変数よりも望ましいです。let 変数よりも、内部変数の方が望ましいです。
  • 仕様ファイル全体の重複を減らすには、let を使用してください。
  • ひとつのテストで使用する変数の定義にはlet を使用せず、テストのit ブロック内部でローカル変数として定義します。
  • トップレベルのdescribe ブロックの内部でlet 変数を定義し、その変数がcontextdescribe ブロックの内部で使用されるようにしないでください。定義する場所は、使用する場所にできるだけ近づけてください。
  • let 変数の定義を別の変数で上書きすることは避けてください。
  • let 、別の変数の定義でしか使われないような変数は定義しないでください。代わりにヘルパーメソッドを使用してください。
  • let! 変数は、定義された順序で厳密に評価する必要がある場合にのみ使用しますletlet変数が参照されるまで評価されないlet ことを let忘れないでください。
  • 例ではsubject を参照しないようにしてください。subject(:name) 、またはlet 変数を使用し、変数が文脈に応じた名前を持つようにしてください。
  • もし例の中でtheが参照されることがないのであれば、名前を付けずにsubject 定義してもかまいません subject

一般的なテストの設定

note
before_all:delete 戦略では動作しません。詳細については、イシュー420379を参照してください。

場合によっては、テスト用の同じオブジェクトを例ごとに再作成する必要はありません。たとえば、プロジェクトとそのプロジェクトのゲストが同じプロジェクトのイシューをテストするために必要なので、1つのプロジェクトとユーザーでファイル全体に対して十分です。

できる限り、before(:all)before(:context) を使用して実装しないでください。 もし実装する場合、これらのフックはデータベーストランザクションの外部で実行されるため、手動でデータをクリーンアップする必要があります。

代わりに、let_it_be 変数とtest-prof gembefore_all フックを使用することで実現できます。

let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }

before_all do
  project.add_guest(user)
end

この結果、このコンテキストで作成されるのはProjectUserProjectMember の3つだけとなります。

let_it_bebefore_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_beallow のようなスタブを持つファクトリでは使用できないことに注意してください。let_it_bebefore(: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.readRails.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 などのメソッドをスタブする必要がある場合は、必ず以下のようにしてください:

  1. File.read を、関心のあるファイル・パスのみにスタブしてください。
  2. 他のファイルパスについては、元の実装を呼び出してください。

そうしないと、コードベースの他の部分からの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 でインデックスが作成されるのを待ちます。
  • そのデータを検索します。
  • テストが期待通りの結果をもたらすことを確認します。

インデックス内の個々のレコードではなく、構造的な変更をチェックするような例外もあります。

note
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 イベントのテスト

caution
Snowplowはcontracts gemを使って 実行時の型チェックを行います。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

categoryaction は省略可能ですが、テストの不具合を避けるために、少なくともcategory を指定する必要があります。例えば、Users::ActivityService は API リクエストの後に Snowplow イベントを追跡する可能性があり、expect_no_snowplow_event は引数が指定されていないときに実行されると失敗します。

スキーマに対する Snowplow コンテキストのテスト

Snowplow schema matcherは、JSON スキーマに対して Snowplow コンテキストをテストすることで、検証エラーを減らすのに役立ちます。schema matcher は以下のパラメータを受け付けます:

  • schema path
  • context

スキーママッチャの仕様を追加するには、次のようにします:

  1. 新しいスキーマをIgluリポジトリに追加し、同じスキーマをspec/fixtures/product_intelligence/ ディレクトリにコピーします。
  2. コピーしたスキーマで、"$schema" キーと値を削除してください。このキーはspecには必要ありませんし、キーが残っているとURLからスキーマを探そうとしてspecが失敗します。
  3. 次のスニペットでスキーマ・マッチャーを呼び出してください:

    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 ファイルに含める必要があることを示しています。

caution
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_at2023-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_statusexpect(response.status).to よりもhave_gitlab_http_status のほうがいいでしょう。 なぜなら、前者はステータスが不一致のときにレスポンスボディを表示できるからです。これは、あるテストが壊れ始めたときに、ソースを編集してテストを再実行することなく、その原因を知りたい場合に非常に便利です。

特に、500 の内部サーバーエラーが表示されたときに便利です。

数値表現の206 よりも、:no_content のような名前付きの HTTP ステータスを優先してください。サポートされているステータスコードの一覧を参照してください。

使用例:

expect(response).to have_gitlab_http_status(:ok)

match_schemamatch_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.rbspec/support/carrierwave.rb のように、特定のドメインに関連付ける必要があります。

ヘルパーモジュールが特定の種類の仕様にのみ適用される場合、config.include の呼び出しに修飾子を追加する必要があります。例えば、spec/support/helpers/cycle_analytics_helpers.rb:libtype: :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.rbspec/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

テストのドキュメントに戻る