はじめに
RSpecは慣れるととても手に馴染むテスティングツールだが、割と癖があってRSpecでテストを書くのに苦労している人も多いのではないだろうか。
自分はまさにそうで、書きたいテストは決まっていてもそれをどう書けばよいか、というところで当初は時間がかかっていたように思う。
実際に実務で何年かRSpecを使ってきて、よく使うパターン(型)のようなものができてきたので、それらをここにまとめてみようと思う。同じように「どう書けばいいか」で躓いている人や書き方をド忘れしてしまった人の助けになれば幸いである。
前提として、Railsアプリケーションを想定した内容になっている。
検証環境、ライブラリ
基本方針
便宜上、以下の方針でパターンを書く。
subject
でテストの主体を明示するit
の引数は省略するFactoryBot
やwebmock
といったライブラリは使わない
Request spec
ステータスコードが xxx であること
# ステータスコード200を期待する場合 subject { get xxx_path } it do is_expected.to eq 200 end
または
subject { get xxx_path } it do expect(response).to have_http_status(200) end
レスポンスボディに xxx が含まれていること
subject { get xxx_path } it do subject expect(response.body).to include('xxx') end
データが新規作成されていること
Fooモデルの件数が1増えていることを確認
subject { post xxx_path } it do expect { subject }.to change(Foo, :count).by(1) end
データが更新されていること
fooのvalue属性が "from"
から "to"
になることを確認
# 実際にはパラメータを渡して、fooが更新対象となるようにするだろう subject { put xxx_path } let(:foo) { Foo.find(1) } # 更新対象のインスタンス it do expect { subject }.to change(foo, :value).from('from').to('to') end
データが削除されていること
Fooモデルの件数が1減っていることを確認
subject { delete xxx_path } it do expect { subject }.to change(Foo, :count).by(-1) end
リダイレクトされること
subject { get xxx_path } # yyy_pathにリダイレクトされることをテスト it do expect { subject }.to redirect_to(yyy_path) end
ファイルのダウンロードができること
レスポンスヘッダの Content-Disposition
から確認する
subject { get xxx_path } # 通常、ファイルダウンロード時はContent-Dispositionヘッダが次のような形式になることから # Content-Disposition: attachment; filename="YOUR_FILENAME.pdf" it do subject expect(response.headers['Content-Disposition']).to include('attachment') expect(response.headers['Content-Disposition']).to include('YOUR_FILENAME.pdf') end
Model spec
次のようなFooモデルクラスがあるとする。
class Foo < ApplicationRecord validates :value, presence: true end
バリデーションにパスすること
subject { foo.valid? } let(:foo) { Foo.new(value: 'abc') } it do is_expected.to be true end
バリデーションエラーとなること
subject { foo.valid? } let(:foo) { Foo.new(value: nil) } it do is_expected.to be false end
バリデーションエラーとなり、期待するエラーが発生していること
subject { foo.valid? } let(:foo) { Foo.new(value: nil) } # 指定した属性でエラーが起きていることをテスト it do subject expect(foo.errros).to include(:value) end # 指定した属性でエラーが起きており、かつメッセージも期待通りであることをテスト it do subject expect(foo.errros.full_messages_for(:value)).to include("can't be blank") end
Job spec
次のようなSampleJobクラスがあり、sample_job
という名称でキューが登録されるものとする。
# app/jobs/sample_job.rb class SampleJob < Application queues_as :sample_job def perform(name) puts "Hello, #{name}!" end end
ジョブがエンキューされること
subject { SampleJob.perform_later('Bob') } # subjectを実行することでエンキューされることをテスト it do expect { subject }.to have_enqueued_job(SampleJob).with('Bob').on_queue('sample_job') end
ジョブが実行され、期待する動作をすること
#perform_enqueued_job
によりジョブが同期的に実行されるため、その後に期待する状態をテストすればOK。
※ 参考 : ActiveJob::TestHelper
subject { SampleJob.perform_later('Bob') } it do perform_enqueued_jobs { subject } # ジョブ実行後に期待する振る舞いを以下に書く end
Mailer spec
次のようなSampleMailerクラスがあり、#send_mail
でメールが送信されるものとする。
# app/mailers/sample_mailer.rb class SampleMailer < ApplicationMailer default from: 'from@example.com' def send_mail mail(to: 'to@example.com', subject: 'title', body: 'body') end end
メール送信処理が実行すること
通常、テスト環境では実際にメールを送信せず、送信されたはずのメールは ActionMailer::Base.deliveries
から参照できる。
※ 参考 : https://railsguides.jp/testing.html#メイラーをテストする
subject { SampleMailer.send_mail } it do expect { subject }.to change(ActionMailer::Base.deliveries, :count).by(1) end
送り主(from)が期待通りであること
subject { SampleMailer.send_mail } it do mail = subject expect(mail.from).to eq 'from@example.com' end
宛先(to)が期待通りであること
subject { SampleMailer.send_mail } it do mail = subject expect(mail.to).to eq 'to@example.com' end
タイトルが期待通りであること
subject { SampleMailer.send_mail } it do mail = subject expect(mail.subject).to eq 'title' end
本文が期待通りであること
subject { SampleMailer.send_mail } it do mail = subject expect(mail.body).to eq 'body' end
モック
外部APIを呼び出すオブジェクトを使っている場合などに使う。
オブジェクトのメソッド呼び出しをモックする
let(:my_obj) { instance_double('MyObj') } before do allow(my_obj).to receive(:my_method).and_return('Hello, world!') allow(MyObj).to receive(:new).and_return(my_obj) end it do my_obj = MyObj.new expect(my_obj.my_method).to eq 'Hello, world!' end
期待するメソッドが呼び出されていること
subject { ... } let(:my_obj) { instance_double('MyObj') } before do allow(my_obj).to receive(:my_method).and_return('Hello, world!') allow(MyObj).to receive(:new).and_return(my_obj) end it do subject expect(my_obj).to have_received(:my_method).once end
共通
その他、Specの種類によらず共通でよく使うパターンを少しだけ。
例外を投げること
subject { 1 / 0 } it do expect { subject }.to raise_error(ZeroDivisionError) end
期待するメッセージとともに例外を投げること
subject { 1 / 0 } it do expect { subject }.to raise_error(ZeroDivisionError).with('divided by 0') end
オブジェクトが期待する属性を持っていること
subject { OpenStruct.new(name: 'Bob') } it do expect(subject).to have_attributes(name: 'Bob') end
配列が期待通りの構造であること
subject { [1, 10, 'Hello, world!'] } it do expect(subject).to match [ 1, 10, 'Hello, world!' ] end
Composing Mathcer を使えばより柔軟なテストができる。
subject { [1, 10, 'Hello, world!'] } it do expect(subject).to match [ eq(1), a_kind_of(Integer), match(/^Hello/), ] end
Hashが期待するキーを持つこと
subject { { key: 'value' } } it do expect(subject).to include(:key) end
Hashが期待するキーバリューを持つこと
subject { { key: 'value' } } it do expect(subject).to include(key: 'value') end
Hashの配列が全て期待するキーを持つこと
subject do [ { key: 'value1' }, { key: 'value2' }, { key: 'value3' }, ] end it do expect(subject).to all(include(:key)) end
正規表現にマッチすること
subject { '123' } it do expect(subject).to match(/^[0-9]+$/) end
nilであること
subject { nil } it do expect(subject).to be_nil end
be_nil
に限らず、nil?
のような ?
で終わるメソッドは be_xxx
として使うことができる。
※ 参考 : Predicate matchers - Built in matchers - RSpec Expectations - RSpec - Relish
あるクラスのサブクラスのインスタンスであること
subject { 'Hello, world!' } it do expect(subject).to be_a(String) # => success expect(subject).to be_a(Object) # => success end
あるクラスの直接のインスタンスであること
subject { 'Hello, world!' } it do expect(subject).to be_an_instance_of(String) # => success expect(subject).to be_an_instance_of(Object) # => fail end