RSpec の change マッチャにおけるレシーバとメッセージ、ブロックの違い
こんにちは。st-1985 です。
RSpecでオブジェクトの状態の変化をテストするには、change
マッチャが便利です。
このマッチャには、レシーバとメッセージを渡す方式と、ブロックを渡す方式の2つの記述方式があります。
今回はレシーバとメッセージを渡す方式を使用していてうまくいかないケースがあったので、そちらについて書こうと思います。
change マッチャ
change
マッチャは、以下のようにレシーバとメッセージ(本例だとobject
と:an_attribute
)を渡して使います。
# my_method を実行すると an_attribute が before から after に変化する事を確認
expect { object.my_method }
.to change(object, :an_attribute)
change(object, :an_attribute)
の箇所はブロックを渡す方式もあります。
# my_method を実行すると an_attribute が before から after に変化する事を確認
expect { object.my_method }
.to change { object.an_attribute }
同じような働きをしますが、内部では
- レシーバとメッセージを渡す:
object.my_method
実行前と後で、メモ化されたレシーバに対して2回メッセージが実行される - ブロックを渡す :
object.my_method
実行前と後で、2回ブロックが実行される
という挙動の違いがあります。
この違いにより、思ったようにテストが動作しない場合があります。
レシーバとメッセージを渡す方式で上手くいかないケース
前述のようにレシーバはメモ化されているため、メッセージがレシーバの状態に依存しており、expect
によってレシーバが更新されない場合はテストがうまくいきません。
例えば、APIリクエストによってDBの状態が更新される事をテストするケースを考えてみます。
expect { api_request }
.to change(
Model.find_by(identifier:'request_identifier'), :an_attribute
) # did not change
Model.find_by(identifier:'request_identifier')
はメモ化されるため、取得した時点の状態のまま変わらず、api_request
でもそのインスタンスは更新されないためan_attribute
の結果は変わりません。
これはブロックを渡す方式にする事で解消可能です。
expect { api_request }
.to change {
Model.find_by(identifier:'request_identifier').an_attribute
} # changed
こちらでは渡したブロックがapi_request
の実行前後で行われるため、データの再取得が行われ変更をテストする事ができます。
まとめ
-
change
マッチャの引数には2種類の方式がある - レシーバとメッセージを渡す方式はレシーバがメモ化されて、そのレシーバに対してメッセージが2回実行される
- ブロックを渡す方式はブロックが2回実行される
- メッセージがレシーバの状態に依存して、レシーバが更新されない場合はブロックを渡す方式を使うと良い
この記事がお役に立てれば幸いです。
おまけ:ブロックを渡す方式だと上手くいかないケースを考える
プライベートメソッドの実行
ブロックを渡す方式ではプライベートメソッドを直接呼び出せないため上手くいきません。
expect { object.my_method }
.to change { object.my_private_method } # NoMethodError
レシーバとメッセージを渡す方式ではうまくいきます。
expect { object.my_method }
.to change(object, :my_private_method) # changed
これは、change
マッチャの内部でレシーバに対して__send__
でメッセージを実行しているためになります。
という事は、ブロックを渡す方式でも__send__
もしくはsend
を利用する事でテストを成立させる事ができます。
expect { object.my_method }
.to change { object.send(:my_private_method) } # changed
send
を使用する是非は置いておいて、プライベートメソッドに関してもレシーバとメッセージを渡す方式でないと成立しないとは言えないのでこのケースは該当しなそうです。
他にケースが思いつかないので、思いついたら追記したいと思います。
Discussion