SSブログ

単体テストのための外部DSL [YokohamaUnit]

プログラムの単体テスト(自動実行されるテストコード)は普通テスト対象となるプログラムと同じ言語で書かれる。 当たり前のようだが必然的にそうでなければならないということではない。

単体テストを専ら関数やメソッドに対するテストであると定義した場合、 言語Aのプログラムの単体テストを記述する言語に求められる最低限の要件は、 「その言語Aの関数やメソッドの呼び出し規約に従ってリンクできる」ということだけだ。 実際Javaの単体テストをGroovyで書くということはおこなわれているようだし、 理論上はC言語の単体テストをSML#で書いたって構わないはずだ。[^1]

それでもテスト対象と同じ言語で単体テストが書かれるのが一般的なのは、

  1. 自明にリンクできる
  2. 開発者自身が単体テストを書くことが多い

といった理由によるだろう。

また、こうした単体テストはある種の内部DSLとして書かれる。 私の理解では、これは単体テストのコードが 単に「言語Aで書かれた別のプログラム」ように見えるのではなく、 「テストの意図をそれにふさわしい見え方で表現したもの」として見えてほしいからだ。

例えばJavaのJUnitであれば、下記のような書き方をする。

assertThat(sut.get(0), is(123));

この書き方は "Assert that sut.get(0) is 123." という英語の文として読み下すことができるので可読性が高いと言われている。 このような考え方はしばしば「ドキュメントとしてのテスト」といわれる。

ところで上記のJUnitの文は詳しく見てみると「2つのレベル」がある。 「テスト対象の呼び出し」に属する部分と「ドキュメント」に属する部分だ。

さっきJUnitの文を英文にパラフレーズするときに sut.get(0) の部分だけは(いわば直接話法のように)そのまま残していた。 この箇所は英文としてそのまま読み下すことができない。 しかしそれはテスト対象の呼び出しの部分だから、 ドキュメントの文法(疑似英語)でなく対象プログラムの文法に属するとみなすことは自然である。

sut.get(0) 以外の部分は「ドキュメント」に属するレベルである。 (123がどちらに属するかについては微妙な点だ)

このドキュメントの部分は「自然」だろうか。 たとえば何故assertとThatの間に空白がなく、ThatのTだけが大文字なのだろうか。 isの前にカンマが来て後に括弧が来るのはなぜだろうか。 つまり結局のところなぜ "Assert that sut.get(0) is 123." と書けないのだろうか。

もちろんその答えはドキュメントのレベルをJavaの内部DSLで記述しているからだ。 しかし、もともとなぜJavaの単体テストをJavaで書いているのかを振り返ってみよう。 それはテスト対象とのリンク要件を容易に満たせるからだった。 しかし先に分割した2つのレベルのうち、 リンク要件を満たす必要があるのは「テスト対象の呼び出し」のレベルだけで、 「ドキュメント」のレベルは別にそうではない。

以上のような考察から、「単体テストのための外部DSL」があってもいいのではないか、 という考えが浮かび上がってくる。 その外部DSLは上述の2つのレベルからなる文法を持ち、 テスト対象とリンクするバイナリにコンパイルされるだろう。

YokohamaUnit はそのようなJava向けの単体テストフレームワークである。 「ドキュメント」部分には独自の文法を持ち、 「テスト対象の呼び出し」の部分にはGroovyを使う。 テストコードは直接JUnitのクラスファイルにコンパイルされる。

次のコードはYokohamaUnitのテストコードとして妥当なものの例である。

*[FizzBuzz]: yokohama.unit.example.FizzBuzz
 
# Test: FizzBuzz test 
 
Assert that `new FizzBuzz().generate(16)` is
`[ "1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8",
   "Fizz", "Buzz", "11", "Fizz", "13", "14", "FizzBuzz", "16" ]`.
*[StringUtils]: yokohama.unit.example.StringUtils
 
# Test: Test cases for `toSnakeCase`
 
Assert that `StringUtils.toSnakeCase(input)` is `expected`
for all input and expected in Table [1].
 
Assert that `StringUtils.toSnakeCase(null)` throws
an instance of `NullPointerException`.
 
| input           | expected          |
| --------------- | ----------------- |
| ""              | ""                |
| "aaa"           | "aaa"             |
| "HelloWorld"    | "hello_world"     |
| "practiceJunit" | "practice_junit"  |
| "practiceJUnit" | "practice_j_unit" |
| "hello_world"   | "hello_world"     |
[1]

外部DSLの選択は文法の不自然さ以外にもいくつかの内部DSLの制約を解消しうる。 例えば(JUnitにおける)例外のテストやパラメタ化テストの問題などである。

次の記事からYokohamaUnitの機能を紹介していきたい。

[^1] 脱線になるが、結合のレベルが上がるほどこの要件は緩和される。 コマンドのテストで求められるのは、コマンドライン呼び出しができることだけだ。 たとえばDejaGnuはTclベースだが、 テスト対象がTclで書かれていなければならないということはない。 またRESTサービスをテストするのに使うクライアント側の言語の選択肢も、 サービス側の実装言語にほとんど左右されないだろう。


nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。