uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

「相互作用中心の試験のためのassertの拡張」の提案

前置き:モックを使った試験の目的は相互作用中心の試験

モックとスタブの違い」の記事にもあるように、モックを使った相互作用中心の試験とは「(一連の)メソッド呼び出しが(しかるべき順序でしかるべき引数で・・・)行なわれたかどうか」を試験するということです。試験対象オブジェクトに対して刺激をしてやって(メソッドコールをしてやって)、それに引き続いてそのオブジェクトが外界に対してどのような一連のメソッドコールを発行するか、ということを確認するということがモックオブジェクトを使った試験(相互作用試験)の本質です。

ただ、このとき「モックオブジェクト」はオブジェクトから「外界」への働きかけを検出するための道具立てに過ぎません。モックオブジェクトは相互作用中心の試験を実現する、一つの手段にすぎません。

assertを使ったサンプルコードにおける問題

ちょっと話はずれるのですが、RubyとかGroovyの本やWeb資料を見てると、プログラムの説明をするためにassertを多用してるのを散見します。例えば、

  a = "ABC" * 2
  assert a == "ABCABC"

のような感じで、assertによって、コードのある時点で、ある条件が成り立っているかどうか、を確認します。上の場合、"ABC" * 2した結果が"ABCABC"であること(=達成したいこと、期待結果)が、コード上一目瞭然です。これはファウラー氏のいう「状態中心の試験」(⇔相互作用の試験=モックを使った試験)に該当するでしょう。ユニットテストの典型的な例でもあります。ちなみに、サンプルコードに関していうと、このようにassertを使ってうれしいのは、検証が容易だということです。推敲や校正の段階でサンプルコードは結構修正するので、コードの正しさを簡単に確かめられることは重要です。

しかし、「ある機能を実行すると、あるメソッドが呼び出される」という機能の説明のためにサンプルコードを示したい場合、assertだとすんなり書けません。例えば、クラスAのメソッドf1が呼び出されたことを確認するために、

  class A {
     public void f1() { println "f1 called" }
  }

  def a = new A()
  a."f1"()
  // "f1 called"が出力される

こんな風な、printlnを使ったサンプルはわかりやすいでしょう。しかしこの場合、結果の確認には、目視が必要です。機械的にチェックができません。assertを使うように書き換えると例えば、

  class A {
     boolean flag = false
     public void f1() { flag = true }
  }

  def a = new A()
  a."f1"()
  assert flag == true

こんな風にできるかもしれません。でもこれって、フラグ変数を使っていて、わかりやすいとは思えません。本当は「メソッドが呼ばれたこと」を確認したいのに、それができないから、フラグ変数というものを導入して、そのフラグ変数の値の変化から間接的にメソッドが呼ばれたということを推測しているだけです。直接的ではない。
あるいは、フラグ変数を使わずに、ByteArrayOutputStreamをつかって出力されたメッセージをキャプチャするという案もあるかもしれませんが、本質的には同じです(フラグ変数の変わりにバイト配列が使われている)。

ユニットテストも同様

上は、サンプルコードの話として示しましたが、「assertを使って、コードが期待を満たすかどうかを自動判定できるようにしたい」という意味では、ユニットテストと同じ動機に基づいた問題です。また、単に実行できるだけではなく、可読性も高めたい、という要望があることも同じです。いずれの場合でも、assertは実行可能ドキュメントとしての意味があるからです。可読性に対する要求は、サンプルコードの方が高いと思いますが、ユニットテストのテストコードの場合だって劣らず重要です。だから、サンプルコードに限らず、ユニットテスト一般にも、「メソッドが呼ばれたかどうかをassertを使ってきれいに書きたい」のです。

そして、モックオブジェクトを使っての相互作用中心の試験は、上の「フラグ変数を作って間接的に確認すること」を大々的にやってるものです。あえていうと、必要悪の化け物みたいな。

何が問題か

問題は、メソッドが呼ばれたことを確認するassertを記述できないことです。さらに、「モックオブジェクト」ではなくて、試験対象オブジェクトオブジェクト以外の「外界」全般に対する記述として、メソッド呼び出しの有無(や順序、引数)をassertで記述することもできません。

解決策(案)

今、可能な手段があるわけではないので、解決策ではありませんが、アイデアとして、以下のようなことができればいいのかなあ、と。

  class A {
     public void f1() { println "f1 called" }
  }

  def a = new A()
  assertInteraction code:{ a."f1"() } with:{
    methodCall(A.f1).withParameterCount(0)
  }

とか、こんな風に「メソッドが呼ばれたことを確認するassert」を書ければ良いんじゃなかろうか。実現するにはMOPとかアスペクトを使いまくる必要があるだろうし、ひょっとしたら処理系自体に手を入れないといけないかもしれませんが。

ここで、架空のメソッドassertInteractionは、2つのクロージャを引数としてとって、1つめのクロージャを実行するに当たって、2番目のクロージャで指定された相互作用の記述(インタラクションビルダーによるもの)を満たしているかを判定します。2つのクロージャはマルチスレッドないしファイバーとして並行的に実行されるべきですね。methodCall()は待ちに入って、A.f1()が呼び出されるまで待ちます。呼ばれなかったときのためにタイムアウトも必要ですね。

・・・・こんなの世の中にないかなー。