Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実践Rspec@nikotama.rb #11

実践Rspec@nikotama.rb #11

架空の実装に対して、自分が実践しているRspecのスタイルを説明しました。参加者とのディスカッションも巻末に載せています。

32e2d4352f049d2c28990dc9e7c1a18a?s=128

Hiroki Kishi

August 26, 2020
Tweet

Transcript

  1. 実践Rspec 2020年8月26日@nikotama.rb

  2. 自己紹介 2016年6月にFablic入社し、以降 Railsエンジニアをやっています。 楽天への吸収合併を経て、ラクマのエ ンジニアをしています。 最近はTypeScriptにハマってます。 岸 洋希 (kissy)

  3. 課題

  4. Controller Service Model DB 5 5 5 5 25 125

    625 合計775パタ⑲ン 全てを行うことは不可能だから、諦めている
  5. Controller Service Model DB 5 5 5 5 25 125

    625 Modelのテストと重複 ServicehModelのテストと重複
  6. Controller Service Model DB 5 5 5 5 Unitテストとして各レイヤのみを意識したテストにすれば 25パタ⑲ン

    で済む! (プラスで軽い結合テストがあってもよいかも)
  7. たとえば class Foo def complex_method response = request(params, headers) object

    = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end 例外が発生したら? 例外が発生したら? 分岐 例外が発生したら? 例外が発生したら? ここだけ握りつぶしてる
  8. 実際どうやってspec書く? • 実際に一つのメソッドに対してspecを書く方法は沢山ありますよね • 今回一例を書いてみたので、ぜひ自分の書き方と違うところや お互いのいいやり方を紹介しあってディスカッションできればと思います

  9. 作っていくぅ

  10. 1.セットアップフェ⑲ズ

  11. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end
  12. 1.セットアップフェ⑲ズ • subject, allow, before, テスト環境のデ⑲タ準備など。 • itやcontextで変わるものを変数化して、各テストフェ⑲ズで切り替える • くり返し使えるテストはshared_examples_for(または

    shared_context)で共通化しておく • hhhということをしてできる限り外部モジュ⑲ルへの依存性を排除する • このフェ⑲ズをしっかりやっておくことで、様々なバリエ⑲ションのテスト が少ないコ⑲ドで書ける!
  13. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  14. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  15. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end ブロック渡しがシンプルで
  16. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end doubleの宣言と一緒に メソッド定義できるのが
  17. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end doubleの宣言と一緒にメソッド定義できるのが
  18. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  19. おまけ: double, spy, partial double いずれも仮の振る舞いをするオブジェクト。 • double ◦ 振る舞いを定義(allow)していないメソッドを呼ぶとエラ⑲

    ◦ 同じオブジェクトに対して戻り値を定義する必要が無いメソッドをいくつも呼ぶ場合は、 いちいちallowするのが面倒 • spy ◦ 振る舞いを定義(allow)していないメソッドを呼ぶとnilが返る ◦ 今後変更されないであろう使い捨てのバッチ(Rake)を書くときは、 spyで済ませちゃうことも • partial double ◦ 「allow(User).to receive(:find) { ... }」と定義されたオブジェクト。 ◦ 実際のオブジェクトを元に、一部だけ振る舞いを定義し直しているのでpartial def spy(*args) double(*args).as_null_object end https://github.com/rspec/rspec-mocks/blob/main/lib/rspec/mocks/example_methods.rb#L120-L122
  20. 2.検証フェ⑲ズ

  21. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end
  22. 検証フェ⑲ズ • mock/stubを活用して外部モジュ⑲ルへの依存を極力排除することで、 mockの実際のメソッドコ⑲ルを手厚く検証できる • 簡単なテ⑲ブル更新であれば、and_call_originalを使ってテ⑲ブルの値 をも検証して良いかもしれない

  23. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end もともとの挙動を使いたいときに使えて
  24. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end .once .twice .exactly(n).time .exactly(n).times .at_least(:once) .at_least(:twice) .at_least(n).time .at_least(n).times .at_most(:once) .at_most(:twice) .at_most(n).time .at_most(n).times 覚えられない
  25. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  26. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  27. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  28. describe Foo do describe '#do_something' do let(:foo) { Foo.new }

    subject { foo.complex_method } context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end end end end class Foo def complex_method response = request(params, headers) object = ResponseHandler.new(response).do_something begin if object.new? AModel.create!(object.to_h) else AModel.update!(object.to_h) end rescue => e logger.error(e) end end end
  29. context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: {

    a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end バリエ⑲ションを増やす context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end context 'リクエストが成功して新しいレコ⑲ド作成に成功した場合' do let(:object) { double(:object, new?: true, to_h: { a: :b}) } let(:response_handler) { double(:response_handler, do_something: object) } before do allow(foo).to receive(:request) { { bar: 1} } allow(ResponseHandler).to receive(:new) { response_handler } allow(AModel).to receive(:create!).and_call_original allow(AModel).to receive(:update!).and_call_original end it 'レコ⑲ドが1件できている' do subject expect(AModel.count).to eq 1 expect(foo).to have_received(:request).with(...).once expect(ResponseHandler).to have_received(:new).with(bar: 1).once expect(response_handler).to have_received(:do_something).once expect(AModel).to have_received(:create!).with({ a: :b }).once expect(AModel).not_to have_received(:update!) end セットアップフェ⑲ズで赤字のところを可変にしておいて 、各context毎に異なる値を設定して様々な挙動を網羅し ていく • double(名前, メソッド名: リタ⑲ン値) • allow(オブジェクト).to receive(メソッド名) { リタ⑲ン値 } • allow(オブジェクト).to receive(メソッド名).and_raise(エラ⑲) 共通部分ごとごっそりコピ⑲して、必要なところだけ変え るのはやめよう • コピペで数千行の追加差分があるPRを見てそっ閉じしました
  30. fizzbazz.run_upto(15) fizz_count = 1 expect(fizzbazz).to have_received(:fizz).exactly(5).times do |n| expect(n).to eq

    fizz_count * 3 fizz_count += 1 end おまけ: 複数回呼ばれるメソッドの引数検証 各メソッドコ⑲ル時の引数を受け取るブロック
  31. おまけ: 他にも使えそうなメソッドたち rspec-mocks/lib/rspec/mocks/syntax.rb • receive_messages rspec-mocks-3.9.1/lib/rspec/mocks/example_methods.rb • double, spy •

    instance_double, instance_spy ◦ クラスを渡すと、そのクラスに実装されたインスタンスメソッドが呼べるdouble/spyが返る • object_double, object_spy ◦ オブジェクトを渡すと、そのオブジェクトに実装されたメソッドが呼べるdouble/spyが返る ◦ 動的に生やされたオブジェクトメソッドまで再現される! • class_double, class_spy ◦ クラスを渡すと、そのクラスに実装されたクラスメソッドが呼べるdouble/spyが返る • stub_const, hide_const ◦ constantsを変えられる。バッチサイズ10,000とかの処理をテスト用に10に書き換えればテストも楽ちん。
  32. 宣伝 ラクマメンバ⑲のテック記事発信はじめました 検索: 「R-Hack ラクマ」「ラクマ Qiita」 いいねhブクマなどよろしくおねがいします!

  33. ご静聴ありがとうございました

  34. Appendix

  35. instance_double, instance_spy $ gem install rspec-mocks $ pry pry(main)> require

    'rspec/mocks/standalone' => true pry(main)> instance_double(String, length: 100000000).length => 100000000 pry(main)> instance_double(String, length: 100000000).downcase RSpec::Mocks::MockExpectationError: #<InstanceDouble(String) (anonymous)> received unexpected message :downcase with (no args) pry(main)> instance_spy(String, length: 100000000).length => 100000000 pry(main)> instance_spy(String, length: 100000000).downcase => #<InstanceDouble(String) (anonymous)>
  36. 発表後の議論1: テストをDRYにすべきかどうか? DRY反対派 • shared_examples, shared_contextを多用すると、テストが失敗した時に 前提条件を追いづらい • ただ、カスタムMatcherやメソッドを作って共通化するなどのことはする DRY賛成派(←自分はこっち)

    • コピペで大量コ⑲ドが差分に現れると(数百q数千単位)、まず読むのが辛い • 似たようなコ⑲ドの中に実は絶妙な差分があるかもしれないのを探すのがキツい • 検証内容(expect)の追加が少ないコ⑲ド追加で可能 • DRY化されたテストの失敗原因を追うのは辛いけど、共通化されてるぶん ポイントさえ見つければ、失敗を直すのも早い
  37. 発表後の議論2: メソッドコ⑲ルを検証すべきか? • すべきじゃない派 ◦ 内部実装に依存しすぎて、IN/OUTに関係ないロジックの変更でもテストが 落ちてしまうのは不便 ◦ テストのIN/OUTh副作用(DB/外部への通信など)さえ合ってれば 内部実装はそこまで見なくてもいいのでは

    • したい派(←自分だけ?) ◦ 一度はデバッグして確認することなので、その確認をコ⑲ドで行っている。 ◦ 自動化しておけば、次に変更するタイミングでも同じクオリティでテストされ、 問題が未然に防げる(んじゃないか) ◦ IN/OUTh副作用以外にどこでどう問題になるか分からない恐怖に対して とりあえずできるところまで細かくテストしている。