RSpecでexitを含むコードをテストする
TL;DR
expect{subject}.to raise_error SystemExit
exit
をテストする状況はそもそも筋が悪い
前置き
こんにちは。私は最近mi
というRails用のマイグレーションジェネレータを作っています。
Railsのジェネレータは内部でThorというライブラリを使用しています。
このライブラリを使用していく上で、一つ問題が発生しました。
処理を途中で切り上げるためにexit
メソッドを使用しないといけない事態に陥ってしまっていました。
問題点
exit
はその時点でプロセスを終了するメソッドです。
このメソッドの問題点の一つに、テストがしづらくなると言うことがあります。
例えば、以下のようなメソッドをテストすることを考えます。
def do_something # ... if cond # ... exit 1 end # ... end
do_something
テストでcond
がtrue
の場合をテストしたい時、通常の方法だとテストを行うことが出来ません。
何故ならばexit
の時点でRubyのプロセスが終了してしまい、テストコード側に処理が戻ってこないためです。
解決策
SystemExit
例外を補足することで対処します。
Rubyではexit
を実現するために例外機構を使用しています。
exit
を呼び出すと、SystemExit
が送出されます。
この例外クラスはStandardError
の子クラスではない為、通常のrescue
では補足されません。
See. module function Kernel.#exit (Ruby 2.3.0)
describe '#do_something' do subject{do_something} it do expect{subject}.to raise_error SystemExit end end
上記コードのようにSystemExit
クラスの例外が発生することを期待する/補足することで、対象メソッドのテストが可能となります。
そもそもの設計論
私は、そもそも奥まったメソッドの内部でexit
を呼び出すべきではないと考えています。
例えば、先ほどのdo_something
メソッドは以下のようにリファクタリングすることが出来るでしょう。
def do_something # ... if cond # ... return 1 end # ... return 0 end def main exit do_something end
このリファクタリングにより、テストの際はdo_something
の戻り値を見ることでテストを行うことが可能です。
この手法はRubyに限らず有効でしょう。
例えばGo言語では、os.Exit()
を使用した場合defer
での後処理が実行されないため、よりこのテクニックが必要とされます。
Rubyだとexit
は例外で実装されているためensure
節が実行されるので、Go言語の様な心配は不要ですね。
ところが、Thorを使用していると上記のようなリファクタリングが不可能です。
Thorでは定義された公開メソッドを順に実行していくのですが、この仕様だと途中のメソッドで処理を終了したい際にexit
するしかなくなってしまいます。
他にも扱いづらい挙動が多いため、mi
では脱Thor(脱Rails Generator)を検討しています。
もし上記のリファクタリングがThorでも可能な方法があれば、教えてくださると助かります。
まとめ
expect{subject}.to raise_error SystemExit
でテストが可能- しかし、
exit
をテスト対象のコードで行わない様な設計ができればそちらのほうがいい