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の定義は
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力を鍛えていきたいと思いました。