前回の投稿で、定数の同値テストの結果を見易くするmatcherを作成した。
今回も類似のネタだが、もう少し細かく制御できるmatcherを作る方法を取り上げる。
概要
- モジュールで定義した定数の同値テストをするためのmatcherを生成するメソッドを実装する。
- 生成したmatcherは、前回紹介したmatcherと比較して以下の特徴を持つ。
- モジュール内の特定の定数グループのみを対象とした比較ができる。
- matcherを生成するので、テストケースを書く際にはモジュール名は書く必要がない。
解決したい問題
以下のようなクラスのrequestの結果をテストしたいケースを考える。
class Widget
RES_SUCCESS = 0
RES_SUSPEND = 1
RES_ERROR = -1
STATUS_OK = 0
STATUS_NG = -1
def request
# RES_XXXXを返す処理
end
end
素直にテストを書くと記述すると以下のようになる。
wid = Widget.new
# requestがRES_ERRORを返す場合
expect(wid.request).to eq Widget::RES_SUCCESS
# => expected: 0
# => got: -1
しかし、これでは失敗したときのメッセージが分かりにくい。
我々は0とか-1とかではなく、RES_SUCCESS
やRES_ERROR
という表記で読みたいのだ。
シンプルなケースでは、前回のconst_eq
でも良い。
しかし、同じ値を持つ定数がクラス内に複数定義されていた場合(今回の場合はRES_SUCCESS
とSTATUS_OK
が同じ値になる)に上手くいかないケースがある。
解決方法
matcherを生成するメソッドを定義
このようなクラスやモジュールの個別の問題に対処するためのmatcherを生成するメソッドを実装する。
def define_const_matcher(mozule, matcher_symbol, &filter)
RSpec::Matchers.define matcher_symbol do |exp_symbol|
match do |act_val|
mozule.const_get(exp_symbol) == act_val
end
failure_message do |act_val|
act_symbol = mozule.constants(true).
select { |c| filter.call(c, mozule.const_get(c)) }.
find { |c| mozule.const_get(c) == act_val }
act_msg = if act_symbol.nil? then " got: invalid value(#{act_val})"
else " got: #{mozule.name}::#{act_symbol}(#{act_val})"
end
exp_val = mozule.const_get(exp_symbol)
"expected: #{mozule.name}::#{exp_symbol}(#{exp_val})\n" << act_msg
end
end
end
matcherを生成
今回の場合はWidgetクラスの定数の中でも、'RES'から始まる一群の定数を対象にテストできるmatcherが欲しい。
そのmatcherを生成するには次のようにdefine_const_matcher
メソッドを呼び出してやればよい。
define_const_matcher(Widget, :widget_res_eq) { |c, _| c.to_s.start_with?('RES') }
使用例
生成したmatcherを先程の例に適用すると読み易いエラーを出力するテストケースを作ることができる。
wid = Widget.new
# requestがRES_ERRORを返す場合
expect(wid.request).to widget_res_eq :RES_SUCCESS
# => expected: Widget::RES_SUCCESS
# => got: Widget::RES_ERROR
補足
- 今回は定数名で対象を絞り込んだが、
define_const_matcher
に渡すブロックを使い、値で絞り込むこともできる。