Rubyの例外クラス設計

具体的には、テストです *2 。例えば foo(1, 2) で wrong number of arguments が投げられることをテストしたいとします。以下のテストだと、wrong number of arguments 以外の ArgumentError が投げられる場合でも合格になってしまいます。

assert_raise(ArgumentError) { foo(1, 2) }

ちゃんとやりたければ、例えばこんな感じのコードを書かないとだめかな。

flag = false
begin
  foo(1, 2)
rescue ArgumentError => e
  raise unless ex.message[/\Awrong number of arguments \(\d+ for \d+\)\z/]
  flag = true
end
assert(flag)
Ruby の例外クラスは分類が粗すぎる or 細かすぎる - まめめも

現状の問題点が「テストしにくい」という点のみであれば、それはテストを行うライブラリ側で解決すべき問題であって、例外クラスの設計とは関係ないと思う。
Test::Unitであればassert_raise()が例外オブジェクトを返すからそれを使えばいいし、RSpecならraise_error()の第2引数にエラーメッセージを指定すればいい。

### Test::Unitの場合
ex = assert_raise(ArgumentError) { hoge(10, 20) }
assert_equal("wrong number of arguments (2 for 1)", ex.message)
### RSpecの場合
proc { hoge(10, 20) } .should raise_error(ArgumentError, "wrong number of arguments (2 for 1)")

もちろん実際には assert_raise_with_message みたいなメソッドにくくりだすとしても、メッセージを文字列比較や正規表現で判定しないといけないのはダサいです。

ださいとは思わないけど、Rubyの実装やバージョンごとにエラーメッセージが違ってたらテストするときに困るよね。実際、Matz Ruby と Rubinius はエラーメッセージが違うことがけっこうあるから、テストケースもかき分ける必要がある。

極端な話、例外にメッセージ文字列を持たせるのではなく、メッセージの種類の数だけ例外クラスがあるべきではないかな *3 。

互換性を考えると難しいかもしれないから、妥協案としてエラーコードを追加するというのはどうだろうか。
たとえばException#codeを追加し、assert_equal(ArgumentError::WRONG_NUMBER_OF_ARGUMENTS, ex.code) とか。

...(snip)...ArgumentError の中の特定の例外だけ拾いたいときに困ります。

そうか、エラーコードを追加したとしても、それでできるのはエラーの分類だけで、特定の例外だけ拾いたい場合には不細工なコードを書かないといけないのか。

begin
  foo(10, 20)
rescue ArgumentError => ex
  raise ex unless ex.code == ArgumentError::WRONG_NUMBER_OF_ARGUMENTS
  ...
end

んー、まあ許容範囲内ではあるか、このぐらいなら。

結局、例外クラスをどこまで細かく分類するかというのは、どういう単位で例外を補足したいかによるのか。なるほど。

yugui 2009/04/25 20:39
ユーザーライブラリが再利用できる語彙が少ないのも気になりますね。こういう意味の例外だからここから派生すべき、みたいな文化が醸成されていない。
ku-ma-me 2009/04/25 21:27
あー、そうなんですよね。Ruby 本体がわかりやすい例外クラス設計指針を示せば、みんなそれに従うんじゃないかな。

Ruby本体じゃなくても、Rails/Merbとかやってくんないかな。
あと例外クラスの設計についてはJava界がいちばん真剣に考えているんじゃないかな。他の言語での例外クラス設計は参考になるかも。