RspecでMockを使う

  • 9
    Like
  • 0
    Comment

https://relishapp.com/rspec/rspec-mocks/docs

記述例は、全てitの中で書かれているものとして読んでください。
letやsubjectに分離して記述する場合は、適宜読み替えてください。

いいね、つけていただいたんで情報足します。
20170119

mockを使ったrspecテストのコツ

はまって試行錯誤した結果のまとめ。

  • publicメソッドのテストに注力する
  • privateメソッドはテストしない方向で
  • 他のメソッドが呼ばれる箇所はそのメソッドを偽装し、他のメソッドをテストするようなことにならないようにする
  • 他のクラスのインスタンス生成箇所は、元のコードの方をプライベートメソッド化し、偽装
  • 他のクラスのインスタンス生成箇所はインスタンス変数に入れて後で使うことが多いので上記のメソッド偽装して、インスタンス生成部分をスキップ。インスタンス変数をinstance_variable_setで直接spec上で突っ込む
  • メソッド内でインスタンス変数を使っている場所は、instance_variable_setでその変数に直接、mockのインスタンス(double)を突っ込む
  • mockのインスタンスのメソッドを呼ぶ箇所は、allowをつかって、メソッド呼び出しの偽装をする
  • mockではなく、実在のインスタンスのメソッドを呼ぶ際に、前提となる準備作業が必要となる場合、allowをつかって、実在のインスタンスに対して、メソッド呼び出しの偽装を実現させて、準備作業自体をスキップさせる
  • 実在のファイルをオープンする箇所で、クロージャーを使って居る場合、File.openで、特定の値を引数にとる場合に限定し、withを使ってFileのopenメソッドを偽装する。ブロック引数には、IOオブジェクトが渡ることを期待されるので、openメソッドの偽装の際に、and_yieldを使ってIOオブジェクトを渡せるようにする。IOオブジェクトは、stringioのインスタンスにしておくと、spec上だけで準備できるので便利?多分。
  • sendメソッド、instance_variable_set、instance_variable_getメソッドあたりをつかうと自由度があがる
  • initialize内で何かのインスタンスを生成している場合、コードの方のメソッド化およびspec上でメソッド呼び出しの偽装をするには手間がかかる。その場合、specでnewを呼ぶ箇所で、allocateを使うとinitializeを呼ぶ前のところまででとまる。必要があれば、そこからinitializeを呼んでもオッケー

モックの作成

モックの作成
mock_obj = double('mock name')

mock_objが生成される。

インスタンスが反応するメソッドを設定する

作成したモックをmethod_nameメソッド呼び出しに反応させる
allow(mock_obj).to receive(:method_name)

メソッド名はシンボルで記述。

通常のインスタンスをmethod_nameメソッド呼び出しに反応させる
sample = Sample.new
allow(sample).to receive(:method_name)

後述の#and_returnメソッド呼び出しがなければ、戻り値はnilとなる。

ブロックを与えると、ブロックの最後の評価結果が返る。

ブロックを与える
sample = Sample.new
allow(sample).to receive(:method_name) { 3 }

モックのオブジェクトだけではなく、通常のインスタンスに対しても
反応させるメソッドを追加可能。

既に同名のメソッドが定義されている場合は、新しく設定したメソッドの呼び出しが
有効になる。

同一インスタンスに対して、反応するメソッドを複数設定したい場合は、
メソッドの数、複数回設定する。

複数設定
sample = Sample.new
allow(sample).to receive(:method_name_1)
allow(sample).to receive(:method_name_2)

メソッドが呼ばれた際に返す値を設定する

メソッドが呼ばれた際に返す値を設定する
sample = Sample.new
allow(sample).to receive(:method_name).and_return("hoge")

sampleインスタンスのmethod_nameメソッドが呼ばれると"hoge"が返るようになる。

sampleインスタンスのmethod_name_1メソッドを呼び出した場合、1が返る。
sampleインスタンスのmethod_name_2メソッドを呼び出した場合、2が返る。

複数メソッドを設定する際、receive_messagesメソッドを使って一括で設定することもできる。

receive_messagesメソッド
sample = Sample.new
allow(sample).to receive_messages(:method_name_1 => 1, :method_name_2 => 2)

メソッドが呼ばれた際に例外を発生させる

例外を発生させる
sample = Sample.new
allow(sample).to receive(:method_name).and_raise(SampleError)

and_raiseメソッドは、次のパターンで呼び出せる。

  • and_raise(ExceptionClass)
  • and_raise("message")
  • and_raise(ExceptionClass, "message")
  • and_raise(instance_of_an_exception_class)

メソッドが呼ばれた際に、Throwを投げる

and_throw(:symbol)
and_throw(:symbol, argument)

メソッドが呼ばれた際にブロックの引数に値を入れる

and_yield
sample = Sample.new
allow(sample).to receive(:method_name).and_yield(1, 2)

sample.method_name do |x, y|
   puts x #=> 1
   puts y #=> 2
end

上記内容の時、xには、1が、yには2が渡される。

これを利用すると、ファイルをオープンする際に、ブロックを伴っていても、
ブロックの引数に、直接IOオブジェクトを渡すことができる。

ブロックの引数に渡すIOオブジェクトをStringIOで生成すると、
実在のファイルパスにファイルと適した内容を用意しなくても済むので
tempfileなどを駆使しなくてもよくなる? ※ 要検証

ファイルをオープンする
string_io = StringIO.new("hogehoge")
allow(File).to receive(:open).and_yield(string_io)

config = String.new
file_path = nil

File.open(file_path, 'r') do |input_file|
    config = input_file.read
end

config #=> "hogehoge"