こんにちは、冬になると首がつりやすくなるラクマの高橋です。
ラクマではRubyを主に使って開発しており、テストフレームワークはRSpecを採用しています。
RSpecの書き方について、最近躓いた3選を今回はお届けします。
検証環境
- ruby: 2.6.5
- rails: 6.0.3.2
- rspec-rails: 3.9.0
module単体に対してテストを書きたい
moduleをいろんなクラスで使用することを想定するとき、テストを特定のクラスに依存させて書くのはよくなさそうです。
そんなとき、どうすればよいか悩んだのですが、こんなふうに書くと、特定のクラスに依存せず、moduleをinclude/extendしたクラスのテストができそうでした。
Class.new { include Module }
して作成したダミーのクラスに対しテストする
# app/models/concerns/good_module.rb module GoodModule def ok 'ok!' end end
# spec/models/concerns/good_module_spec.rb describe GoodModule do context '特異メソッドとして使う場合' do let(:dummy_class) { Class.new { extend GoodModule } } subject { dummy_class.ok } it { expect(subject).to eq 'ok!' } end context 'インスタンスメソッドとして使う場合' do let(:dummy_class) { Class.new { include GoodModule } } let(:dummy_class_instance) { dummy_class.new } subject { dummy_class_instance.ok } it { expect(subject).to eq 'ok!' } end end
こんな感じで書いてみます。
$ bundle exec rspec spec/models/concerns/good_module_spec.rb .. Finished in 0.0046 seconds (files took 1.79 seconds to load) 2 examples, 0 failures
実行できました!
インスタンス変数の書き込みメソッドをモックしたい
モックを利用すると、テストしやすい場面があると思います。
# app/models/concerns/sendable.rb module Sendable def send_mail(message, email) message.to = email puts 'send!' end end
こんな感じの処理を作りましたが、messageクラスをまだ作っていません。 モックにして先にテストだけしてみたいと思います。
# spec/models/concerns/sendable_spec.rb describe Sendable do let(:dummy_class) { Class.new { extend Sendable } } let(:message) { double('message') } subject { dummy_class.send_mail(message, 'example@example.com') } before do allow(message).to receive(:to) end it { subject } end
いざ実行すると…
$ bundle exec rspec spec/models/concerns/sendable_spec.rb F Failures: 1) Sendable Failure/Error: message.to = email #<Double "message"> received unexpected message :to= with ("example@example.com") # ./app/models/concerns/sendable.rb:6:in `send_mail' # ./spec/models/concerns/sendable_spec.rb:9:in `block (2 levels) in <top (required)>' # ./spec/models/concerns/sendable_spec.rb:15:in `block (2 levels) in <top (required)>' Finished in 0.01396 seconds (files took 1.74 seconds to load) 1 example, 1 failure
received unexpected message
となってしまいました。
これだと想定通りに動きません。
attr_accessorの対象となるインスタンス変数に=をつける
ただ、よく考えてみると、attr_accessorの定義は
Module#attr_accessor (Ruby 3.0.0 リファレンスマニュアル)
def name @name end def name=(val) @name = val end
こうなので、モックも
# spec/models/concerns/sendable_spec.rb before do # =を追加する allow(message).to receive(:to=) end
これが正しかったのでした。
メソッドチェーンをモックする
例えば、Time.zone.now.hour
のようなメソッドチェーンが返す値をモックしたいときがあると思います。
# app/models/concerns/checkable.rb module Checkable def ok Time.zone.now.hour == 10 end end
早速RSpecを書いてみます。
# spec/models/concerns/checkable_spec.rb describe Checkable do let(:dummy_class) { Class.new { extend Checkable } } subject { dummy_class.ok } before do allow(Time).to receive('zone.now.hour').and_return(10) end it { expect(subject).to be true } end
$ bundle exec rspec spec/models/concerns/checkable_spec.rb F Failures: 1) Checkable Failure/Error: allow(Time).to receive('zone.now.hour').and_return(10) Time does not implement: zone.now.hour # ./spec/models/concerns/checkable_spec.rb:11:in `block (2 levels) in <top (required)>'
勘で書いてみましたが、やっぱり無理でした。。
receive_message_chain
を使う
そういう場合は、RSpec側にこれを想定したメソッドが用意されているのでそれを利用します。
# spec/models/concerns/checkable_spec.rb before do allow(Time).to receive_message_chain(:zone, :now, :hour).and_return(10) end
$ bundle exec rspec spec/models/concerns/checkable_spec.rb . Finished in 0.00916 seconds (files took 1.65 seconds to load) 1 example, 0 failures
これで、無事、メソッドチェーンでも、特定の値を返すことができました。
終わり
RSpecのドキュメントを読むと、こんな書き方できたんだという発見が多いです。 テストに助けられることが多いので、今後も転ばぬ先の杖として、需要にあったテストを書けるように、RSpec力を鍛えていきたいと思いました。