RSpec の Compound Matcher Expressions を not_to と一緒に使用する方法
はじめに
RSpec 3 から登場した Compound Matcher Expressions (合成マッチャ式) を使って、検証したい複数の事象を一つの expect
にまとめることができるようになりました。
しかし、Compound Matcher Expressions では not_to
(「〜でないこと」) を使うことはできません。この記事では、Compound Matcher Expressions を使う際に、not_to
と同等のテストを書くための方法を紹介します。
TL;DR
Define Negated Matcher と組み合わせることで、not_to
に相当する反転マッチャを定義することができる。
RSpec::Matchers.define_negated_matcher :not_start_with, :start_with
RSpec::Matchers.define_negated_matcher :not_end_with, :end_with
expect(alphabet).to not_start_with("あ").and not_end_with("ん")
RSpec::Matchers.define_negated_matcher :not_end_with, :end_with
expect(alphabet).to start_with("a").and not_end_with("ん")
そもそも Compound Matcher Expressions (合成マッチャ式) とは何か?
RSpec のブログにわかりやすいコード例が紹介されています。
Compound Matcher Expressions - New in RSpec 3: Composable Matchers
上記リンク先のコード例を引用して簡単に説明します。
アルファベットが "a"
で始まり、"z"
で終わることを検証したかったとします。RSpec 3 以前では、このように書く必要がありました。
expect(alphabet).to start_with("a")
expect(alphabet).to end_with("z")
RSpec 3 で登場した Compound Matcher Expressions を使うと、上記のコードを以下のように書くことができるようになります。
expect(alphabet).to start_with("a").and end_with("z")
and
を使うことで、二つの expect
を一つにまとめることができました。
and
は二つの条件を両方満たす場合にパスしますが、どちらか片方のみ条件を満たす場合にパスする or
も用意されています。
たとえば、信号機の色が、赤、青、黄色のいずれかであることを検証したい場合は以下のように書くことができます。
expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
問題点
本題に戻ります。
上記のコード例では「〜であること」を検証しているため、to
を使っています。「アルファベットが "a"
で始まり、"z"
で終わること」や「信号機の色が、赤、青、黄色のいずれかであること」のように。
ときには、「〜でないこと」を検証したいこともあるでしょう。しかし、残念ながら Compound Matcher Expressions で not_to
を使うことはできません。
Failures:
1) should not start with "a" and end with "z"
Failure/Error: expect(alphabet).not_to start_with('a').and end_with('z')
NotImplementedError:
`expect(...).not_to matcher.and matcher` is not supported, since it creates a bit of an ambiguity. Instead, define negated versions of whatever matchers you wish to negate with `RSpec::Matchers.define_negated_matcher` and use `expect(...).to matcher.and matcher`.
「expect(...).not_to matcher.and matcher
はサポートされていません。」というエラーが表示されてしまいます。
解決策
実は先ほどのエラーメッセージの中に解決策が書かれていました。Define Negated Matcher (反転マッチャ) を使います。
Define Negated Matcher は、マッチャの条件を反転させたものを新たに定義することができます。
start_with
は文字列の先頭が、引数で与えられた文字 (列) であることを検証するマッチャですが、反対に文字列の先頭が引数で与えられた文字 (列) ではないことを検証するマッチャを以下のように定義することができます。
RSpec::Matchers.define_negated_matcher :not_start_with, :start_with
define_negated_matcher
の第一引数に、定義したいマッチャの名前を指定し、第二引数に、反転したい対象のマッチャを指定します。
これで、文字列の先頭が、引数で与えられた文字 (列) ではないことを検証できる not_start_with
マッチャが使えるようになりました。
これを使って、「アルファベットが "あ"
で始まらず、"ん"
で終わらないこと」を検証するコードを Compound Matcher Expressions と Define Negated Matcher を使って以下のように書くことができます。
RSpec::Matchers.define_negated_matcher :not_start_with, :start_with
RSpec::Matchers.define_negated_matcher :not_end_with, :end_with
expect(alphabet).to not_start_with("あ").and not_end_with("ん")
もちろん、「〜であること」と「〜でないこと」を同時に検証することもできます。たとえば「アルファベットが "a"
で始まり、"ん"
で終わらないこと」を検証するコードは以下のように書くことができます。
RSpec::Matchers.define_negated_matcher :not_end_with, :end_with
expect(alphabet).to start_with("a").and not_end_with("ん")
Compound Matcher Expressions はどういうときに有用か
Compound Matcher Expressions の利点は、単にまとめて書けることだけではありません。まとめて書かないと都合が悪いときに活躍します。
標準出力せずに他の検証もしたい場合
たとえば、以下のような実装コードがあったとします。
def alphabet
puts 'abcdefghijklmnopqrstuvwxyz'
end
上記のコードは puts
を使ってアルファベットを標準出力しています。
上記のコードにおいて、例外が発生しないことだけを検証するシンプルなテストを書くと以下のようになります。
expect { alphabet }.not_to raise_error
上記のテストは正常に動作しますが、以下のように標準出力にアルファベットが表示されてしまいます。
abcdefghijklmnopqrstuvwxyz
.
Finished in 0.00476 seconds (files took 0.1303 seconds to load)
1 example, 0 failures
当然といえば当然なのですが、この標準出力を RSpec では表示しないようにするために、標準出力を検証する方法があります。
expect { alphabet }.to output("abcdefghijklmnopqrstuvwxyz\n").to_stdout_from_any_process
上記のコードは alphabet
メソッド実行後の出力が "abcdefghijklmnopqrstuvwxyz\n"
であることを検証しています。
上記のテストを実行しても画面上にはアルファベットが表示されません。このように標準出力をチェックすると画面上には出力されないというテクニックが使えます。
ところが、例外が発生しないかどうかの検証と、出力の検証を別々の expect
で書くとやはりアルファベットが出力されてしまいます。
expect { alphabet }.not_to raise_error
expect { alphabet }.to output("abcdefghijklmnopqrstuvwxyz\n").to_stdout_from_any_process
なぜなら、expect { alphabet }.not_to raise_error
のほうで alphabet
メソッドが実行されて標準出力されてしまうからです。
これを解決するために、Compound Matcher Expressions が使えます。
RSpec::Matchers.define_negated_matcher :not_raise_error, :raise_error
expect { alphabet }.to not_raise_error.and output("abcdefghijklmnopqrstuvwxyz\n").to_stdout_from_any_process
例外が発生しないことを検証するマッチャ not_raise_error
を Define Negated Matcher を使って定義します。そして、not_raise_error
と output
を組み合わせて、一つの expect
で表現します。
これにより、例外が発生しないことと出力が正しいことを検証しつつ、RSpec 実行時に画面にアルファベットを出力しないようにすることができます。
exit せずに他の検証もしたい場合
もう一つ別の例を挙げましょう。
def bye
puts 'Bye!'
exit
end
Bye!
と表示して exit するコードです。
このコードに対して、Hello!
と出力されずに、exit
されることを検証したかったとします。
素直に書くと以下のようなコードになります。
it 'does not output hello' do
expect { bye }.not_to output("Hello!\n").to_stdout_from_any_process
end
it 'exits' do
expect { bye }.to raise_error(SystemExit)
end
上記のテストは、失敗はしませんが正しくテストできていません。
Finished in 0.01792 seconds (files took 0.11834 seconds to load)
1 example, 0 failures
テストが成功している際に表示される .
が表示されていません。また、テストは 2 つあるにも関わらず 1 example
となっています。
実装コード内で exit するようなコードがある際に RSpec を実行すると、実際に exit
が実行された時点で RSpec が終了してしまいます。実装コードを実行するプロセスと RSpec を実行するプロセスが同じためです。
実は、それを解消する方法として、SystemExit
という例外を補足すると、exit せずに次のテストに進むことができます。
上記のテストコードはまさにそのテクニックが使われているのですが、標準出力を検証する expect
を実行した段階で exit されてしまっているのでそこで RSpec が終了してしまいます。
これを解決するためには、やはり Compound Matcher Expressions を使って「Hello!
と出力されないこと」と「exit
されること」を一つの expect
で表現します。
RSpec::Matchers.define_negated_matcher :not_output, :output
it 'does not output hello and exits' do
expect { bye }.to not_output("Hello!\n").to_stdout_from_any_process.and raise_error(SystemExit)
end
上記のテストコードを実行すると正しくテストできます。
.
Finished in 0.02079 seconds (files took 0.1141 seconds to load)
1 example, 0 failures
まとめ
Compound Matcher Expressions で not_to
を使うことはできませんが、代わりに Define Negated Matcher を使って not_to
に相当するマッチャを定義することで同等の検証を行う方法を紹介しました。
最後の「Compound Matcher Expressions はどういうときに有用か」は若干こじつけ感がありますが、実際に自分がテストコードを書くなかで使ったテクニックです。
Ruby で CLI ツールを作る際に、標準出力したり、エラー時に exit したりする実装にすることはあるかと思います。
そういうときに Compound Matcher Expressions や Define Negated Matcher を使うと、検証したいテストが簡潔に書けるようになります。
Discussion