# Everyday Rails - RSpecによるRailsテスト入門 [Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)のメモ - 開発環境 - Ruby:3.2.2 - Rails:7.0.6 - rspec-rails:6.0.3 - testデータベースはsqlite ## 1. イントロダクション - 著者が考えるテストの原則 - テストは信頼できるものであること - テストは簡単に書けること - テストは簡単に理解できること(今日も将来も) 何か学びたいものがあるときはコピペするのではなく、自分でコードをタイピングした方が良い ## 2. RSpecのセットアップ 作業用ブランチの作成 ``` $ git checkout -b my-02-setup origin/01-untested ``` `rspec-rails`のインストール ```ruby group :development, :test do # Railsで元から追加されているgemは省略 gem 'rspec-rails' end ``` - Gemfileに記載するのは`rspec-rails`だけでいい - `rspec-core`等のその他の関連gemも一緒にインストールされる - 非Railsアプリの場合は個別にインストールが必要 テストデータベースの作成 `config/database.yml`を確認して以下のコマンドを実行 ``` $ bin/rails db:create:all ``` RSpecのインストール ``` $ bin/rails generate rspec:install ``` RSpecの出力をデフォルト形式からドキュメント形式に変更 ```diff # .rspec --require spec_helper + --format documentation ``` - `--warnings`フラグを追加するとgemから出力された警告を全て表示する - 実際のアプリ開発ではこのフラグは有効だが、学習目的なら非推奨 RSpecの実行 ``` $ bundle exec rspec ``` RSpecの実行コマンドを省略するための`binstub`を作成 ``` $ bundle binstubs rspec-core ``` - `bin/rspec`が作成される RSpecのジェネレータがデフォルトで生成するファイルの設定を変更 ```diff # config/application.rb module Projects class Application < Rails::Application config.load_defaults 7.0 + config.generators do |g| + g.test_framework :rspec, + fixtures: false, + view_specs: false, + helper_specs: false, + routing_specs: false + g.factory_bot false + end end end ``` スペックファイルの命名規則 - `app/helpers/projects_helper.rb`をテストする場合 - `spec/helpers/projects_helper_spec.rb` - `lib/my_library.rb`をテストする場合 - `spec/lib/my_library_spec.rb` ## 3. モデルスペック 作業用ブランチの作成 ``` $ git checkout -b my-03-models origin/02-setup ``` ### モデルスペックの構造 モデルスペックの基本構成 - 期待する結果をまとめて記述(describe)する - どんなモデルなのか、どんな振る舞いをするのか - example(`it`で始まる1行)一つにつき、結果を一つだけ期待する - exampleが失敗した箇所を特定しやすくなる - どのexampleも明示的である - 可読性 - 各exampleの説明は動詞で始まっている。shouldではない。 - 可読性 ### モデルスペックを作成する Userモデルのモデルスペックを作成 ``` $ bin/rails g rspec:model user ``` ### RSpecの構文 `expect()`構文を使う(`should`構文は非推奨) - [rspec/rspec-expectations](https://github.com/rspec/rspec-expectations) - [rspec.info/features/3-12/rspec-expectations/](https://rspec.info/features/3-12/rspec-expectations/) ### バリデーションをテストする - 正常系だけでなく、異常系(エラーが発生する条件)もテストする - テストを書いている最中にモデルが持つべきバリデーションについて考えれば、バリデーションの追加を忘れにくくなる ### クラスメソッドとスコープをテストする Noteモデルのモデルスペックを作成 ``` $ bin/rails g rspec:model note ``` ### マッチャについてもっと詳しく - be_valid - [RSpec::Rails::Matchers#be_valid](https://rspec.info/documentation/6.0/rspec-rails/RSpec/Rails/Matchers.html#be_valid-instance_method) - include - [include matcher](https://rspec.info/features/3-12/rspec-expectations/built-in-matchers/include/) - [RSpec::Matchers#include](https://www.rubydoc.info/gems/rspec-expectations/RSpec%2FMatchers:include) - eq - [RSpec::Matchers#eq](https://rubydoc.info/github/rspec/rspec-expectations/RSpec%2FMatchers:eq) ### describe、context、before、afterを使ってスペックをDRYにする - [before and after hooks](https://rspec.info/features/3-12/rspec-core/hooks/before-and-after-hooks/) - [RSpec::Core::Hooks](https://rspec.info/documentation/3.12/rspec-core/RSpec/Core/Hooks.html) - [The basic structure (describe/it)](https://rspec.info/features/3-12/rspec-core/example-groups/basic-structure/) `describe`と`context`の使い分け - `describe` - クラスやシステムの機能に関するアウトラインを記述する時に使う - `context` - 特定の状態に関するアウトラインを記述する時に使う [RSpecの(describe/context/example/it)の使い分け #RSpec - Qiita](https://qiita.com/uchiko/items/d34c5d1298934252f58f) ### まとめ - 期待する結果は能動形で明示的に記述すること - exampleの結果がどうなるかを動詞を使って説明する - チェックする結果はexample一つに付き一個だけ - 起きてほしいことと、起きてほしくないことをテストすること - exampleを書くときは両方のパスを考え、その考えに沿ってテストを書く - 境界値テストをすること - もしパスワードのバリデーションが4文字以上10文字以下なら、8文字のパスワードをテストしただけで満足しないでください。4文字と10文字、そして3文字と11文字もテストするのが良いテストケースです。 - 可読性を上げるためにスペックを整理すること - `describe`と`context`はよく似たexampleを分類してアウトライン化する - `before`ブロックと`after`ブロックは重複を取り除く - しかし、テストの場合はDRYであることよりも読みやすいことの方が重要 ## 4. 意味のあるテストデータの作成 作業用ブランチの作成 ``` $ git checkout -b my-04-factories origin/03-models ``` ### アプリケーションにファクトリを追加する Userモデルのファクトリを追加。 ``` $ bin/rails g factory_bot:model user ``` 生成されたファイルを以下のように編集。 ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do first_name { "Aaron" } last_name { "Sumner" } email { "tester@example.com" } password { "dottle-nouveau-pavilion-tights-furze" } end end ``` - データを囲む`{}`は必須(Rubyのブロック) - [【翻訳】factory_bot 4.11で非推奨になった静的属性(static attributes) #Ruby - Qiita](https://qiita.com/jnchito/items/81637bbdf66c2662eacf) `FactoryBot.build`→新しいテストオブジェクトをメモリ内に保存する `FactoryBot.create`→アプリケーションのテスト用データベースにオブジェクトを永続化する ### シーケンスを使ってユニークなデータを生成する ```diff # spec/factories/users.rb FactoryBot.define do factory :user do first_name { "Aaron" } last_name { "Sumner" } + sequence(:email) { |n| "tester#{n}@example.com" } password { "dottle-nouveau-pavilion-tights-furze" } end end ``` ### ファクトリで関連を扱う noteとprojectのファクトリを作成 ``` $ bin/rails g factory_bot:model note $ bin/rails g factory_bot:model project ``` ```diff # spec/factories/notes.rb FactoryBot.define do factory :note do message { "My important note." } association :project - association :user + user { project.owner } end end ``` - 修正前は、メモのファクトリが関連するプロジェクトを作成する際に関連するユーザー(プロジェクトに関連するowner)を作成し、それから2番目のユーザー(メモに関連するユーザー)を作成する - 上記の修正によりデフォルトでユーザーが1人しか作成されなくなる ### ファクトリ内の重複をなくす Projectモデルのテストに使用するProjectファクトリの重複をなくす ファクトリ内の重複をなくす方法は以下の2パターン 1. ファクトリの継承 1. `trait`の使用 #### 1.ファクトリの継承 ```ruby # spec/models/project_spec.rb require 'rails_helper' RSpec.describe Project, type: :model do 省略 describe "late status" do it "is late when the due date is past today" do project = FactoryBot.create(:project_due_yesterday) expect(project).to be_late end it "is on time when the due date is today" do project = FactoryBot.create(:project_due_today) expect(project).to_not be_late end it "is on time when the due date is in the future" do project = FactoryBot.create(:project_due_tomorrow) expect(project).to_not be_late end end end ``` ```ruby # spec/factories/projects.rb FactoryBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description { "Sample project for testing purpose" } due_on { 1.week.from_now } association :owner factory :project_due_yesterday do due_on { 1.day.ago } end factory :project_due_today do due_on { Date.current.in_time_zone } end factory :project_due_tomorrow do due_on { 1.day.from_now } end end end ``` #### 2.traitの使用 ```ruby # spec/models/project_spec.rb require 'rails_helper' RSpec.describe Project, type: :model do 省略 describe "late status" do it "is late when the due date is past today" do project = FactoryBot.create(:project, :due_yesterday) expect(project).to be_late end it "is on time when the due date is today" do project = FactoryBot.create(:project, :due_today) expect(project).to_not be_late end it "is on time when the due date is in the future" do project = FactoryBot.create(:project, :due_tomorrow) expect(project).to_not be_late end end end ``` ```ruby # spec/factories/projects.rb FactoryBot.define do factory :project do sequence(:name) { |n| "Test Project #{n}" } description { "Sample project for testing purpose" } due_on { 1.week.from_now } association :owner trait :due_yesterday do due_on { 1.day.ago } end trait :due_today do due_on { Date.current.in_time_zone } end trait :due_tomorrow do due_on { 1.day.from_now } end end end ``` - `spec/models/project_spec.rb`で使われている`be_late`マッチャについて - RSpecに定義されているマッチャではない - Projectモデル(`app/models/project.rb`)に`late`または`late?`という名前の属性またはメソッドが存在し、それが真偽値を返すようになっていれば`be_late`はメソッドや属性の戻り値が true になっていることを検証してくれる ### コールバック - [factory_bot/GETTING_STARTED.md#callbacks](https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#callbacks) - このドキュメントはいつ消えてもおかしくないので非推奨(公式の情報ではある) - 現在の公式ドキュメントは[the factory_bot book](https://thoughtbot.github.io/factory_bot/) ### ファクトリを安全に使うには - ファクトリを使うとテスト中に予期しないデータが作成されたり、無駄にテストが遅くなったりする原因になる - 可能な限り`FactoryBot.create`よりも`FactoryBot.build`を使う - テストデータベースにデータを追加する回数が減るので、パフォーマンス面のコストを削減できる ## 5. コントローラスペック 作業用ブランチの作成 ``` $ git checkout -b my-05-controllers origin/04-factories ``` ### コントローラスペックの基本 Homeコントローラのテストを作成 ``` $ bin/rails g rspec:controller home --controller-specs --no-request-specs ``` ### 認証が必要なコントローラスペック Projectコントローラのテストを作成 ``` $ bin/rails g rspec:controller projects --controller-specs --no-request-specs ``` ### HTML以外の出力を扱う Taskコントローラのテストを作成 ``` $ bin/rails g rspec:controller tasks --controller-specs --no-request-specs ``` ### まとめ - コントローラのテストはRailsやRSpecから完全にはなくなっていないものの、最近では時代遅れのテストになっている - コントローラのテストは対象となる機能の単体テストとして最も有効活用できるときだけ使うのがよい。使いすぎないように注意する ## 6. システムスペックでUIをテストする システムスペック(system specs)は、受入れテスト(Acceptance Tests)、または統合テスト(Integration Tests)と呼ばれることもある 作業用ブランチの作成 ``` $ git checkout -b my-06-system origin/05-controllers ``` ### システムスペックで使用するgem `Capybara`gemをインストール ```ruby # Gemfile group :test do gem 'capybara' end ``` ### システムスペックの基本 Projectのシステムテストを作成 ``` $ rails generate rspec:system projects ``` - `click_button`を使うと、起動されたアクションが完了する前に次の処理へ移ってしまうことがあるので、`click_button`を実行した`expect{}`の内部で最低でも1個以上のエクスペクテーションを実行し、処理の完了を待つようにするのが良い - `scenario`はCapybaraの構文で、RSpecの`it`と`example`のalias - [capybara - RSpec: describe, context, feature, scenario? - Stack Overflow](https://stackoverflow.com/questions/11643747/rspec-describe-context-feature-scenario) - モデルスペックでは1つのexampleにつき1つのexpectだったのに対して、システムスペックでは1つのexample、もしくは1つのシナリオで複数のエクスペクテーションを書いて良い - システムスペックは時間がかかるため ### CapybaraのDSL - [Capybara公式ドキュメント](https://rubydoc.info/github/teamcapybara/capybara/master) ### システムスペックをデバッグする - テスト失敗時の画面をHTMLファイルとして保存するメソッド(`tmp/capybara`に保存される) - [save_and_open_page](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session#save_and_open_page-instance_method) - 保存して開く - 保存して自動で開くには[Launchy gem](https://rubygems.org/gems/launchy)が必要(Launchy gemがないと保存のみ) - [save_page](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Session#save_page-instance_method) - 保存のみ ```ruby # Gemfile group :test do gem 'launchy' end ``` ### JavaScriptを使った操作をテストする Taskのシステムテストを作成 ``` $ rails generate rspec:system tasks ``` ドライバに関して - Rack::Testドライバは、速くて信頼性が高いが、JavaScriptの実行はサポートしていない - [selenium-webdriver gem](https://rubygems.org/gems/selenium-webdriver) - CapybaraのデフォルトのJavaScriptドライバ - Rails 5.1以降ではデフォルトでインストールされている RSpecの設定ファイルに関して ```ruby # spec/rails_helper.rb Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } ``` - 上記の行を有効にすることで、RSpec関連の設定ファイルを`spec/support`ディレクトリに配置することができる - `spec/rails_helper.rb`に直接設定を書き込まなくて済む `spec/support/capybara.rb`を作成し、ドライバの設定を追加する ```ruby # spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end config.before(:each, type: :system, js: true) do driven_by :selenium_chrome end end ``` - `js: true`のタグがついているシステムテストのみ`selenium_chrome`ドライバを使用する `js: true`タグの付け方 ```ruby # spec/system/tasks_spec.rb require 'rails_helper' RSpec.describe "Tasks", type: :system do scenario "user toggles a task", js: true do # 省略 end end ``` ### ヘッドレスドライバを使う ```diff # spec/support/capybara.rb RSpec.configure do |config| config.before(:each, type: :system) do driven_by :rack_test end config.before(:each, type: :system, js: true) do - driven_by :selenium_chrome + driven_by :selenium_chrome_headless end end ``` ### JavaScriptの完了を待つ - デフォルトではCapybaraはボタンが現れるまで2秒待つ。2秒待っても表示されなければ諦める ```ruby # spec/support/capybara.rb Capybara.default_max_wait_time = 15 ``` - 上記のようにすることで、待ち時間の変更が可能 - 上記の設定だとテスト全体に影響するので、テスト毎に待ち時間を変更したい場合は`using_wait_time`を使う↓ ```ruby scenario "runs a really slow process" do using_wait_time(15) do # テストを実行する end end ``` ### スクリーンショットを使ってデバッグする - [take_screenshot](https://api.rubyonrails.org/classes/ActionDispatch/SystemTesting/TestHelpers/ScreenshotHelper.html#method-i-take_screenshot)メソッド - テスト中のブラウザのスクショを作成するメソッド - JavaScriptドライバでのみ使える - 画像ファイルはデフォルトで`tmp/capybara`に保存される - そもそもシステムスペックのテストは、失敗したら自動で`tmp/screenshots`にスクショが保存される - `selenium-webdriver`をドライバに指定している時には自動保存された - Rack::Testドライバの場合は自動保存されなかった ### システムスペックとフィーチャスペック - フィーチャスペックは非推奨。システムスペックを使う。 ## 7. リクエストスペックでAPIをテストする 作業用ブランチの作成 ``` $ git checkout -b my-07-requests origin/06-system ``` ### リクエストスペックとシステムスペックの比較 - RSpecでのAPI関連のテストは`spec/reuqests`ディレクトリに配置する - リクエストスぺックではCapybaraは使わずに、get, post, delete, patchなどのメソッドを使う ### GETリクエストをテストする `app/controllers/api/projects_controller.rb`のためのリクエストスペックを作成 ``` $ bin/rails g rspec:request projects_api ``` - `spec/requests/projects_apis_spec.rb`が生成される - 本書ではその後`projects_api_spec.rb`にリネームしている(好みの問題、しなくてもいい) ### コントローラスペックをリクエストスペックで置き換える `spec/controllers/home_controller_spec.rb`を置き換えるリクエストスペックを作成 ``` $ bin/rails g rspec:request home ``` - `spec/requests/homes_spec.rb`が生成されるので、`home_spec.rb`にリネーム `spec/controllers/projects_controller_spec.rb`を置き換えるリクエストスペックを作成 ``` $ bin/rails g rspec:request project ``` - `spec/requests/projects_spec.rb`が生成される Deviseの`sign_in`ヘルパーをリクエストスペックに追加する方法 - [How To: sign in and out a user in Request type specs (specs tagged with type: :request) · heartcombo/devise Wiki](https://github.com/heartcombo/devise/wiki/How-To:-sign-in-and-out-a-user-in-Request-type-specs-(specs-tagged-with-type:-:request)) - 本書では`(2) A more complicated example:`の方法を採用 コントローラスペックよりも統合スペック(システムスペックとリクエストスペック)を使うべき - Railsにおけるコントローラスペックは重要性が低下し、かわりにより高いレベルのテストの重要性が上がってきているため - 統合スペックの方がアプリケーションのより広い範囲をテストすることができるため ### まとめ - 最近ではアプリケーション同士がやりとりする機会が増えてきているため、Railsで作成したAPIをテストすることの重要性は徐々に上がってきている - テストスイートを自分のAPIのクライアントだと考えるようにする ## 8. スペックをDRYに保つ 作業用ブランチの作成 ``` $ git checkout -b my-08-dry-specs origin/07-requests ``` ### サポートモジュール ログイン処理のテストコードをサポートモジュールに切り出す ```ruby # spec/support/login_support.rb module LoginSupport def sign_in_as(user) visit root_path click_link "Sign in" fill_in "Email", with: user.email fill_in "Password", with: user.password click_button "Log in" end end RSpec.configure do |config| config.include LoginSupport end ``` サポートモジュールに切り出したメソッドを使う ```ruby # spec/system/projects_spec.rb require 'rails_helper' RSpec.describe "Projects", type: :system do # include LoginSupport scenario "user creates a new project" do user = FactoryBot.create(:user) sign_in_as user # 省略 end end ``` - サポートモジュールのincludeは上記のようにサポートモジュール内で`RSpec.configure`を記述する方法と、システムスペック内のテスト毎に明示的にサポートモジュールをincludeする方法もある - `include LoginSupport` Deviseの`sign_in`ヘルパーをシステムスペックで使う方法 ```diff # spec/rails_helper.rb RSpec.configure do |config| # 省略 # Deviseのヘルパーメソッドをテスト内で使用する + config.include Devise::Test::IntegrationHelpers, type: :system end ``` ### let で遅延読み込みする `before`と`let` - `before` - `before`ブロックを使うと`describe`や`context`ブロックの内部で、各テストの実行前に共通のインスタンス変数をセットアップできる - `before`ブロックでテストデータをセットアップする際は、インスタンス変数に格納する - `before`の中に書いたコードは`describe`や`context`の内部に書いたテストを実行するたびに毎回実行される。これはテストに予期しない影響を及ぼす恐れがある。また、そうした問題が起きない場合でも、使う必要のないデータを作成してテストを遅くする原因になることもある。 - 要件が増えるにつれてテストの可読性を悪くする - `let` - `let`は呼ばれたときに初めてデータを読み込む、「遅延読み込み」を実現するメソッド - `let`でテストデータをセットアップする際は、ローカル変数に格納する - `let`は`before`ブロックの外部で呼ばれるため、セットアップに必要なテストの構造を減らすこともできる `let`と`let!` - `let!`は遅延読み込みされない - `let!`はブロックを即座に実行する。そのため内部のデータも即座に作成される - [let and let! - rspec.info](https://rspec.info/features/3-12/rspec-core/helper-methods/let/) `let!`を使う際の注意点 - コードを読む際は`let`と`let!`の見分けが付きにくく、うっかり読み間違えてしまう可能性がある - このわずかな違いを確認するためにコードを読み返すようであれば、`before`とインスタンス変数に戻すことも検討する - [My issues with Let](https://thoughtbot.com/blog/my-issues-with-let) ### shared_context (contextの共有) `let`を使うと複数のテストで必要な共通のテストデータを簡単にセットアップすることができる。一方、`shared_context`を使うと複数のテストファイルで必要なセットアップを行うことができる。 `shared_context`の使い方 ```ruby # spec/support/contexts/project_setup.rb RSpec.shared_context "project setup" do let(:user) { FactoryBot.create(:user) } let(:project) { FactoryBot.create(:project, owner: user) } let(:task) { project.tasks.create!(name: "Test task") } end ``` ```diff # spec/controllers/tasks_controller_spec.rb require 'rails_helper' RSpec.describe TasksController, type: :controller do - let(:user) { FactoryBot.create(:user) } - let(:project) { FactoryBot.create(:project, owner: user) } - let(:task) { project.tasks.create!(name: "Test task") } + include_context "project setup" # 省略 end ``` - [Using shared_context - rspec.info](https://rspec.info/features/3-12/rspec-core/example-groups/shared-context/) ### カスタムマッチャ - マッチャは必ず名前付きで定義され、その名前をスペック内で呼び出すときに使う - マッチャには`match`メソッドが必要 - 典型的なマッチャは以下の2つの値を利用する - 期待される値(expected value、マッチャをパスさせるのに必要な結果) - 実際の値(actual value、テストを実行するステップで渡される値) ```ruby # spec/support/matchers/content_type.rb RSpec::Matchers.define :have_content_type do |expected| match do |actual| begin actual.content_type.include? content_type(expected) rescue ArgumentError false end end # 失敗メッセージ failure_message do |actual| "Expected \"#{content_type(actual.content_type)} " + "(#{actual.content_type})\" to be Content Type " + "\"#{content_type(expected)}\" (#{expected})" end # 否定の失敗メッセージ failure_message_when_negated do |actual| "Expected \"#{content_type(actual.content_type)} " + "(#{actual.content_type})\" to not be Content Type " + "\"#{content_type(expected)}\" (#{expected})" end def content_type(type) types = { html: "text/html", json: "application/json", } types[type.to_sym] || "unknown content type" end end # エイリアスを作成 RSpec::Matchers.alias_matcher :be_content_type, :have_content_type ``` ```diff # spec/controllers/tasks_controller_spec.rb require 'rails_helper' RSpec.describe TasksController, type: :controller do include_context "project setup" describe "#show" do it "responds with JSON formatted output" do sign_in user get :show, format: :json, params: { project_id: project.id, id: task.id } - expect(response.content_type).to include "application/json" + expect(response).to have_content_type :json end end # 省略 end ``` カスタムマッチャの可否 - 新しいマッチャがあると、メンテナンスしなければならないコードが増える。カスタムマッチャにはその価値があるか?その結論はあなたとあなたのチームで決める必要がある [shoulda-matchers gem](https://github.com/thoughtbot/shoulda-matchers):便利なマッチャがたくさんあるgem ### aggregate_failures (失敗の集約) RSpecはテスト内で失敗するエクスペクテーションに遭遇するとそこで即座に停止して残りのステップは実行されない。 RSpec3.3の新機能`aggregate_failures`は他のエクスペクテーションも続けて実行できる。これによりエクスペクテーションが失敗した原因がようわかるようになる。 - [RSpec::Matchers#aggregate_failures](https://rspec.info/documentation/3.12/rspec-expectations/RSpec/Matchers.html#aggregate_failures-instance_method) - [RSpec 3.3 has been released! - Expectations: New aggregate_failures API](https://rspec.info/blog/2015/06/rspec-3-3-has-been-released/) - [実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド - 1. 特定のエクスペクテーション群をまとめて検証できる(aggregate_failures メソッド)](https://qiita.com/jnchito/items/3a590480ee291a70027c#1-%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%A8%E3%82%AF%E3%82%B9%E3%83%9A%E3%82%AF%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E7%BE%A4%E3%82%92%E3%81%BE%E3%81%A8%E3%82%81%E3%81%A6%E6%A4%9C%E8%A8%BC%E3%81%A7%E3%81%8D%E3%82%8Baggregate_failures-%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89) ### テストの可読性を改善する - [Acceptance Tests at a Single Level of Abstraction](https://thoughtbot.com/blog/acceptance-tests-at-a-single-level-of-abstraction) - このテクニックの基本的な考えはテストコード全体を、内部で何が起きているのか抽象的に理解できる名前を持つメソッドに分割すること。あくまで抽象化が目的なので、内部の詳細を見せる必要はない。 ## 9. 速くテストを書き、速いテストを書く ### RSpecの簡潔な構文 - [subject - rspec.info](https://rspec.info/features/3-12/rspec-core/subject/) - [僕がRSpecでsubjectを使わない理由 - give IT a try](https://blog.jnito.com/entry/2021/10/09/105651) - [RSpec::Core::MemoizedHelpers#is_expected](https://rspec.info/documentation/3.12/rspec-core/RSpec/Core/MemoizedHelpers.html#is_expected-instance_method) [shoulda-matchers gem](https://github.com/thoughtbot/shoulda-matchers)を使うための設定 ```diff # Gemfile group :test do + gem 'shoulda-matchers' end ``` ```diff # spec/rails_helper.rb RSpec.configure do |config| # 省略 end + Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end + end ``` shoulda-matchers gemのメソッドの使用例 ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # 省略 it { is_expected.to validate_presence_of :first_name } it { is_expected.to validate_presence_of :last_name } it { is_expected.to validate_presence_of :email } it { is_expected.to validate_uniqueness_of(:email).case_insensitive } end ``` - [lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb](https://github.com/thoughtbot/shoulda-matchers/blob/main/lib/shoulda/matchers/active_model/validate_presence_of_matcher.rb) - [lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb](https://github.com/thoughtbot/shoulda-matchers/blob/main/lib/shoulda/matchers/active_record/validate_uniqueness_of_matcher.rb) ### モックとスタブ - モック(mock) - モックは本物のオブジェクトのふりをするオブジェクトで、テストのために使われる - モックはテストダブル(test doubles)と呼ばれる場合もある - モックはこれまでファクトリやPOROを使って作成したオブジェクトの代役を務める - モックはデータベースにアクセスしないため、テストにかかる時間は短くなる - スタブ(stub) - スタブはオブジェクトのメソッドをオーバーライドし、事前に決められた値を返す - スタブとは、呼び出されるとテスト用に本物の結果を返すダミーメソッドのこと - スタブをよく使うのはメソッドのデフォルト機能をオーバーライドするケース。特にデータベースやネットワークを使う処理が対象。 テスト内でオブジェクトをモック化するためにテストダブルを使う場合、できるかぎり検証機能付きのテストダブル(verified double)を使う(例:user→User) もしあなたがモックやスタブにあまり手を出したくないのであれば、それはそれで心配しないでください。本書でやっているように、基本的なテストにはRubyオブジェクトを使い、複雑なセットアップにはファクトリを使う、というやり方でも大丈夫です。 [RSpec における double / spy / instance_double / class_double のそれぞれの違いについて](https://zenn.dev/noraworld/articles/ruby-double-spy-instance-double-class-double) ### タグ - [--tag option - rspec.info](https://rspec.info/features/3-12/rspec-core/command-line/tag/) #### 特定のテストだけ実行する方法 ```ruby it "processes a credit card", focus: true do # 省略 end ``` ``` $ bundle exec rspec --tag focus ``` - `focus`タグを持つスペックだけを実行する 以下の設定をして`bundle exec rspec`を実行すると、`focus`タグを持つテストが1個以上ある場合は`focus`タグを持つテストだけを実行し、`focus`タグを持つテストが0個なら全テストを実行する ```ruby # spec/spec_helper.rb RSpec.configure.do |config| config.filter_run_when_matching :focus # 省略 end ``` #### 特定のテストをスキップする方法 ``` $ bundle exec rspec --tag "~slow" ``` - タグ名の前に`~`(チルダ)をつけると、そのタグのついたテストをスキップする 以下の設定をすると、`slow`タグがついたテストは常にスキップされる ```ruby # spec/spec_helper.rb RSpec.configure.do |config| config.filter_run_excluding slow: true # 省略 end ``` ### テストを並列に実行する - [parallel_tests gem](https://github.com/grosser/parallel_tests) ## 10. その他のテスト 作業用ブランチの作成 ``` $ git checkout -b my-10-testing-the-rest origin/09-test-faster ``` ### ファイルアップロードのテスト [Capybara::Node::Actions#attach_file](https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Actions#attach_file-instance_method) - Active Storageでは`config/storage.yml`でアップロードしたファイルの保存先を指定する - テスト環境でのファイル保存先のデフォルトは`tmp/storage` テスト後にアップロードされたファイルを自動削除する設定 ```ruby RSpec.configure do |config| # 省略 config.after(:suite) do FileUtils.rm_rf(ActiveStorage::Blob.service.root) end end ``` - `.gitignore`にファイル保存先のディレクトリが含まれているか確認する - Active Storageの場合はRailsが最初から設定してくれている ファイルアップロードのテストの流れ - 1.ファイルアップロードのステップを含む、スペックファイルを作成する - スペック内ではテストで使うファイルを添付する - 2.テスト専用のアップロードパスを設定(確認)する - 3.テストスイートが終わったらファイルを削除するようにRSpecを設定する ### バックグラウンドワーカーのテスト [Job specs - rspec-rails](https://rspec.info/features/6-0/rspec-rails/job-specs/job-spec/) サンプルアプリの`geocode`メソッドを確認 ``` $ rails db:seed $ rails c Loading development environment (Rails 7.0.6) irb(main):001:0> u = User.find(10) User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 10], ["LIMIT", 1]] => #<User id: 10, email: "morris.kuvalis@upton-pfeffer.test", created_at: "2024-01-06 02:44:52.172421000 +0000", updated_at: "2024-01-06 02:44:52.172421000 +0000", first_name: "Nickie", last_name: "A... irb(main):002:0> u.location => nil irb(main):003:0> u.geocode TRANSACTION (0.1ms) begin transaction User Update (1.2ms) UPDATE "users" SET "updated_at" = ?, "location" = ? WHERE "users"."id" = ? [["updated_at", "2024-01-06 02:45:23.194895"], ["location", "Melbourne, Victoria, AU"], ["id", 10]] TRANSACTION (3.1ms) commit transaction => true irb(main):004:0> u.location => "Melbourne, Victoria, AU" ``` ログイン処理のシステムスペックを作成 ``` $ bin/rails g rspec:system sign_in ``` - `rspec-rails`ではバックグラウンドジョブをテストするために、`queue_adapter`に`:test`を指定する必要がある - [RSpec::Rails::Matchers#have_enqueued_job](https://rspec.info/documentation/6.0/rspec-rails/RSpec/Rails/Matchers.html#have_enqueued_job-instance_method) - このマッチャはブロックスタイルの`expect`と組み合わせないとエラーになることに注意 ジョブがアプリ内のコードを適切に呼び出していることを確認するテストを作成 ``` $ bin/rails g rspec:job geocode_user ``` - [Geocoder gem](https://github.com/alexreisner/geocoder) ### メール送信をテストする `UserMailer`のメーラースペックを作成 ``` $ bin/rails g rspec:mailer user_mailer ``` - [email_spec gem](https://rubygems.org/gems/email_spec) - メール関連のマッチャを提供するgem サインアップのシステムスペックを作成して、Mailerをテスト。 ``` $ bin/rails g rspec:system sign_up ``` - メールはバックグラウンドプロセスで送信されるため、テストコードは `perform_enqueued_jobs`ブロックで囲む必要がある - このヘルパーメソッドは、このスペックファイルの最初で`include`している `ActiveJob::TestHelper`モジュールが提供している - `perform_enqueued_jobs`メソッドを使えば`ActionMailer::Base.deliveries`にアクセスし、最後の値を取ってくることができる - この場合、最後の値はユーザーが登録フォームに入力したあとに送信されるウェルカムメールになる [Message Chains - rspec-mocks](https://rspec.info/features/3-12/rspec-mocks/working-with-legacy-code/message-chains/) ### Webサービスをテストする - [vcr gem](https://github.com/vcr/vcr) - [webmock gem](https://github.com/bblimke/webmock) 外部にHTTPリクエストを送信するテストでVCRを使うように設定 ```ruby # spec/support/vcr.rb require "vcr" VCR.configure do |config| config.cassette_library_dir = "#{::Rails.root}/spec/cassettes" config.hook_into :webmock config.ignore_localhost = true config.configure_rspec_metadata! end ``` VCRを使う際の注意点 - テストに使っている外部APIの仕様が変わってしまっても、カセットが古くなっていることを知る術がない - Railsの開発者の多くはプロジェクトのバージョン管理システムからカセットファイルを除外することを選択します。これは新しい開発者が最初にテストスイートを実行した際に、 必ず自分でカセットを記録するようにするためです。 - この方法を採用する前に、一定の頻度でカセットを自動的に再記録する方法を検討しても良いかもしれません。この方法を使えば、後方互換性のないAPIの変更を比較的早く検知することができます。 - [Automatic re recording - VCR documentation](https://benoittgt.github.io/vcr/#/cassettes/automatic_re_recording) - APIのシークレットトークンやユーザーの個人情報といった機密情報をカセットに含めないようにする - VCRのサニタイズ用のオプションを使う - [Filter sensitive data - VCR documentation](https://benoittgt.github.io/vcr/#/configuration/filter_sensitive_data) ## 11. テスト駆動開発に向けて 作業用ブランチの作成 ``` $ git checkout -b my-11-tdd origin/10-testing-the-rest ``` ### フィーチャを定義する - テストに必要な情報をコメントとして書き始めてから、そのコメントをテストコードに置き換えていく ### レッドからグリーンへ `projects`テーブルに`completed`カラムを追加 ``` $ bin/rails g migration add_completed_to_projects completed:boolean $ bin/rails db:migrate ``` テスト駆動開発の信条は「テストを前進させる必要最小限のコードを書く」 新たなテスト実装する前と後に`bundle exec rspec`を実行してテストスイート全体をチェックする ## 12. 最後のアドバイス - 小さなテストで練習する - 統合スペックを最初に書こうとする - 基本のプロセスは、モデルスペック→コントローラスペック→システムスペック(またはリクエストスペック) - 慣れてきたら上記の逆でまずシステムスペックから考える - システムスペックをパスさせるために開発していると、他のレベルでテストした方が良い機能が見つかる - よくできたシステムスペックはそのフィーチャが関連するテストのアウトラインを導き出してくれる ## Railsのテストに関するさらなる情報源 ### RSpec - [RSpec documentation](https://rspec.info/documentation/) - [Effective Testing with RSpec 3](https://pragprog.com/titles/rspec3/effective-testing-with-rspec-3/) - RSpec公式からも推奨されている書籍 - [Better Specs](https://www.betterspecs.org/) - ベストプラクティス集 - [Railscasts](http://railscasts.com/) - [#275 How I Test](http://railscasts.com/episodes/275-how-i-test?language=ja&view=asciicast) - [RSpecのGoogleグループ](https://groups.google.com/g/rspec) - 自力で答えが見つけられない時はここで質問するのが一番 ### Rails - [Agile Web Development with Rails 7](https://pragprog.com/titles/rails7/agile-web-development-with-rails-7/)