【各章まとめ】Everyday Rails - RSpecによるRailsテスト入門 - 第8章 スペックをDRYに保つ
復習用まとめ
本章で学ぶこと
- ワークフローをサポートモジュールに切り出す
- テスト内でインスタンス変数を再利用するかわりにletを使う
- shared_contextに共通のセットアップを移動する
- カスタムマッチャの作成
- テストを集約して、複数のスペックを一つにする
サポートモジュールに、ログインのフローを切り出す
複数のスペックでの何回も書かれている処理を共通処理として切り出す。例えば、ログイン処理など。ポイントは、メソッド名称を見ただけで何をしているのか一目でわかるように切り出すこと。
切り出し方
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
呼び出し方
上記のようにmoduleに、「config.include XxxxxSupport」と記述するか、呼び出すスペック内にてinclude
RSpec.feature "Projects" ,type::feature do include LoginSupport # ←使うスペックでincludeする scenario "user creates a new project" do # ... end end
Device gemを使っている場合のログインフローの切り出し方
特に上記のように画面操作をシュミレートしなくても、フィーチャーテストにおいてDeviceのヘルパーをつかうことで、ログイン状態にすることができる。
まずは、rails_helper.rbに次の一行を追加
RSpec.configure do |config| # ... config.include Devise::Test::IntegrationHelpers, type: :feature end
RSpec.feature "Projects" ,type::feature do scenario "user creates a new project" do user = FactoryBot.create(:user) sign_in user # セッションを作成する visit root_path # セッション作成後に遷移させることを忘れずに # ... end end
letで遅延読み込み
なぜletを使うか?
beforeブロックでテストデータを用意する場合、下記のデメリットがある。
- テスト実行のたびに毎回実行され、不要データが作成されテスト速度が遅くなる可能性がある。
- 要件が増えるにつれ可読性が悪くなる。
letの使い方
RSpec.describe Note, type: :model do let(:user) { FactoryBot.create(:user) } let(:project) { FactoryBot.create(:project, owner: user) } # このテストではletが呼び出される it "is valid with a user, project, and message" do note = Note.new( message: "This is a sample note.", user: user, project: project, ) expect(note).to be_valid end # このテストではletが呼び出されず、無駄なデータが作成されない it "is invalid without a message" do note = Note.new(message: nil) note.valid? expect(note.errors[:message]).to include("can't be blank") end end
let!を使う必要がある場合
テスト内で明示的にletの値が呼び出されない場合、例えば検索のテスト等でテスト実行前にデータを用意しておく必要がある場合など、let!を使うことでテスト前にlet内部のコードが呼び出される仕組みとなっている。
# これより上のコードは略 describe "search message for a term" do let!(:note1) do FactoryBot.create( :note, project: project, user: user, message: "This is the first note") end let!(:note2) do FactoryBot.create( :note, project: project, user: user, message: "This is the second note") end let!(:note3) do FactoryBot.create( :note, project: project, user: user, message: "First, preheat the oven.") end context "when a match is found" do it "returns notes that match the search term" do expect(Note.search("first")).to include(note1, note3) end end # letとした場合、このテストではデータが全く作成されなくなってしまう # let!にすることで、テスト実行前にデータが作成された状態になる context "when no match is found" do it "returns an empty collection" do expect(Note.search("message")).to be_empty expect(Note.count).to eq 3 end end end
shared_context
テストにおける条件(=context)、セットアップを共有する。毎回同じセットアップコードを書いているならば、shard_contextで共通化できないかを考えてみる。
共通化の方法
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
使う側の実装
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 eq "application/json" end end . . end
カスタムマッチャ
読みにくいエクスペクテーションを分かりやすいカスタムマッチャとして切り出す。処理をメソッドでまとめ、保守性の高い名前を付けることとやってることは同じ。
RSpec の重要な信条のひとつは、人間にとっての読みやすさです。
spec/support/matchers以下にファイルを作成
spec/support/matchers/content_type.rb
RSpec::Matchers.define :have_content_type do |expected| match do |actual| content_types = { html: "text/html", json: "application/json", } actual.content_type == content_types[expected.to_sym] end end
...一旦ここまで。