[Ruby] よく使うRspecのレシピ集(Rspec3.3)
モバイルアプリサービス部の五十嵐です。
最近Rspecをガッツリ書いたので、調べたことをユースケースごとにまとめてみます。
対象バージョンはRspec3.3です。
リフレクション
Rubyのリフレクションを使用したテストの書き方です。
インスタンス変数を操作したい
インスタンス変数を取得したい場合はinstance_variable_get
、設定したい場合はinstance_variable_set
を使います。また、instance_variable_set
でモックを仕込むことでレシーバオブジェクトのインスタンス変数や動作を操作することができます。
obj = Person.new(name: 'Hoge') # initializerで@nameにnameがセットされる想定 name = obj.instance_variable_get('@name') expect(name).to eq 'Hoge' obj.instance_variable_set('@name', 'Fuga') expect(obj.name).to eq 'Fuga'
プライベートメソッドを実行したい
obj.send(:method)
を使います。
expect(obj.send(:private_method)).to ...
引数を渡す場合は、send
メソッドの第二引数以降に渡したい引数をつけます。
expect(obj.send(:private_method, ref1, ref2, ...)).to ...
モック
呼び方はテストダブルやモックやスタブなどもっと細かい分類もありますが、ここでは細かい違いを考慮せず「モック」に統一して書きます。
単純なモックオブジェクトを作りたい
モックオブジェクトを作るにはdouble
を使います。
obj = double('obj')
モックオブジェクトにメソッドを持たせたい場合は、第二引数以降にKeyとValueを指定することで実現できます。
obj = double('obj', count: 10) expect(obj.count).to eq 10
メソッドのモックを作りたい
double
でもメソッドと返り値を指定できましたが、レシーバとなるオブジェクトがモックオブジェクトではなく存在するオブジェクトの場合はallow(obj).to receive(:method)
を使います。
allow(obj).to receive(:method)
メソッドに返り値をもたせたい場合は、.and_return
を使います。
allow(obj).to receive(:method).and_return(true) expect(obj.method).to be_truthy
また、呼び出しごとに返り値を変えたい場合は、.and_return
に複数の引数を指定します。
allow(obj).to receive(:method).and_return(true, false) expect(obj.method).to be_truthy expect(obj.method).to be_falsy
メソッドチェインするメソッドのモックを作りたい
メソッドチェインするメソッドのモックを作る場合、allow(obj).to receive(:method)
を組み合わせることでも書くことはできますが、receive_message_chain
を使えばもっと簡単に書くことができます。
allow(obj).to receive_message_chain(:method1, :method2).and_return(true) expect(obj.method1.method2).to be_truthy
yieldがあるメソッドのモックを作りたい
yield
を含むメソッドのブロック内の動作を検証したい場合、allow(obj).to receive(:method)
に.and_yield
をチェインしてブロックの引数の値を設定します。また、yield
を含むメソッドでyield
が呼ばれた回数を検証したい場合は、yield_control
マッチャーが使えます。
allow(obj).to receive(:each).and_yield(1).and_yield(2).and_yield(3) expect{ |b| obj.send(:each, &b) }.to yield_control.exactly(3)
モックのメソッドが呼ばれることを検証したい
3通りの方法があります。
一つはallow
をexpect
に変える方法です。allow
は定義したモックメソッドが呼ばれなくても何も起きませんが、expect
にすることで呼ばれたことを検証できます。また、.with
を使うことで引数も検証できます。
expect(obj).to receive(:method) obj.method expect(obj).to receive(:method).with(true) obj.method(true)
expect
を使うとexpect
の後に実際の処理を書くことになりますので、処理と検証が逆転してしまいます。allow
とhave_received
を組み合わせることで、処理→検証の順番にすることができます。
allow(obj).to receive(:method) obj.method expect(obj).to have_received(:method)
モックオブジェクトで行うならspy
を使います。spy
はdouble
と似たような機能ですが、have_received
マッチャーを使うことができるようになります。
obj = spy('obj') expect(obj).to have_received(:method)
メソッド内のローカル変数にモックを仕込みたい
オブジェクトのローカル変数など、簡単にモックにできない場所にはallow_any_instance_of
が使えます。allow_any_instance_of
で定義されたクラスのインスタンスの動きを操作できます。
allow_any_instance_of(DB::Client).to receive(:select).and_return([]) adaptor = DB::Adaptor.new expect(adaptor.select).to eq [] # DB::Adaptor.selectの中でDB::Client.selectが実行されている想定
テストデータの永続化
Rspecのテストは、デフォルトではトランザクション内で行われ、テストが終了するとデータは破棄されます。テストデータを永続化させるにはRspec.configureのuse_transactional_fixtures
をfalse
にします。しかしほとんどの場合、テストデータを永続化させるとテストの順序などを考える必要があり、テストが書きにくくなると思います。
部分的にテストデータを永続化する場合は、.specファイル内の永続化したいスコープ内でself.use_transactional_fixtures = false
を実行します。こうするとスコープを抜けてもテストデータが残っているので、スコープ内のafter
またはafter(:all)
でテストデータを忘れずに削除しましょう。
気をつけている点
そのほか、テストを書く上で気をつけている点です。
letやsubjectを使う
before
を多用していたら、let
やsubject
に置き換えられないか考えます。let
は実行時に遅延評価され、複数回使われてもスコープ内では同じオブジェクトが使用されます。また、let!
は即時実行です。
before
はデフォルトでbefore(:all)before(:each)
です。つまりテストが実行されるたびにbefore
ブロック内も実行されます。before
は必要があるときだけ使いましょう。
またsubject
はlet
と同じ機能ですが、テストのSubject(主語)であることを明示したいときに使います。
it内は簡潔にする
it
の中はなるべく簡潔に記述したほうが、「このテストは何をしているのか」が分かりやすくなります。それ以外はlet
やbefore
に移動しましょう。
テストが書きにくくなったらリファクタリングする
テストが書きにくくなったらリファクタリングをしたほうがいいというサインだと思います。見直すポイントは以下のような点があると思います。(まだ語れるほど自信はないのですが...)
- クラスが大きくなりすぎていないか(なりすぎている場合は責務を考えて分割する)
- クラス間やメソッド間の依存性を少なくする
- 依存性の方向が一方向になるように意識する
- スコープを小さくする
割れ窓を作らない
当たり前のことですが、テストは常に通るようにしておきます。1つでも失敗するテストを許してしまうとその状態に慣れてしまい、どんどんと失敗するテストが増えていき最終的に手がつけられなくなります。
まとめ
Rspecには便利なモック機能がたくあんあり、大抵のテストには困らないと思います。逆にこれらを使ったとしてもテストに困るようであればコードの設計を見直したほうがいいと思っています。私も早くテストが書きやすいコードが自然と書けるようになりたいと思います。
参考
- Relish
- rspec/rspec-mocks
- Ruby - 使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」 - Qiita
- [RSpec3]テストデータはどこへ消えた
- RSpec での悲観ロックのテスト — アクトインディ技術部隊報告書
- ほか多数
ありがとうございます。