(第三回)JCUnitで2次方程式を解くプログラムをテストする。
はじめに
本稿執筆中に、JCUnit 0.6.0にバグがあることとユーザビリティを向上する必要があることがわかり、0.6.2をリリースした。
本稿を試すにあたっては以下のようにpom.xml
を更新して欲しい。
<dependencies> <dependency> <groupId>com.github.dakusui</groupId> <artifactId>jcunit</artifactId> <version>[0.6.2,)</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>1.3</version> <scope>test</scope> </dependency> </dependencies>
hamcrest
ライブラリへの依存は今回のJCUnitへの変更とは関係ないのだが、本稿の記述範囲で使用するためにここに含めた。
お手数をおかけするがよろしくお願い申し上げたい。
また、今回からここに例題のソースコードを格納することにした。
本稿の記事を読む折には、git clone https://github.com/jcunit-team/jcunit-examples.git
として欲しい。
もしなにかあればコメント欄などでご連絡頂きたい。
(第三回)JCUnitで2次方程式を解くプログラムをテストする。
第一回で多くのテストが失敗していたが、その結果を分析して二次方程式ソルバーの仕様書を更新した。(「出力される解」については言い回しを変えた)
QuadraticEquationクラス仕様 version 2
これをJCUnitを用いたテストとして実現してみよう。 なお、今回ステップ・バイ・ステップで作るJCUnitのテストは以下の場所に完成版がおいてある。
このコードの解説記事としてお読みいただくと時間の節約になるかもしれない。
因子と水準を設計・実装する。
a
,b
,c
の三つはいずれも整数で境界値は100
と-100
である。
これら一個一個を因子(Factor)とし、それらについてテストに用いるべき水準を列挙するとこんなふうになろうか。
水準 | 種別 |
---|---|
1 | 正常 |
0 | 異常(a ),正常(b ,c ) |
-1 | 正常 |
100 | 境界 |
101 | 異常 |
-100 | 境界 |
-101 | 異常 |
Integer.MAX_VALUE |
異常 |
Integer.MIN_VALUE |
異常 |
「ソフトウェアテストHAYST法入門」3などで"FL表"と呼ばれている表にあたる。
ここでは各水準は100やInteger.MIN_VALUE
といった具体的な値だが、実務の大規模で複雑なテストにおいては、各水準は抽象的に定義しなけらばならない場合が多い。
また、a
,b
,c
はそれぞれ別の因子なので、二次方程式ソルバーのような簡単なソフトウェアをテストするならともかく、普通は別の表にまとめたほうがよかろうと思う。もしも、テスト設計書をドキュメントとして作るならば、だが。
水準0についてだが、a == 0
の場合は異常値(制約1)だが、b
, c
についてはこの制約は関係ないので正常値である。
それはともかく、これをJCUnitの記法で書くならこうなる。
@FactorField(intLevels = {1, 0, -1, 100, 101, -100, -101, Integer.MAX_VALUE, Integer.MIN_VALUE}) public int a; @FactorField(intLevels = {1, 0, -1, 100, 101, -100, -101, Integer.MAX_VALUE, Integer.MIN_VALUE}) public int b; @FactorField(intLevels = {1, 0, -1, 100, 101, -100, -101, Integer.MAX_VALUE, Integer.MIN_VALUE}) public int c;
非常に単純で上で作った表をintLevels = {...}
の中に写しとっているだけだ。
制約を設計・実装する。
まず、制約1を実装する。
制約1:
a
が0の場合、例外が送出される。(問題4:a==0)
ここで使うアノテーションは@Condition
である。
因子a
にはその水準が0
の場合は異常であるという制約がある。
これをJavaのメソッドとして実現すると以下のようになる。
@Condition public boolean aIsNonZero() { return this.a != 0; }
戻り値はその制約に従っている(違反していない)場合にtrue
, 違反している場合にfalse
を返すことになっている。注意して欲しい。
なお制約を実装するメソッドは、boolean
を返す引数なしのものである必要がある。
これに反するとJCUnitは実行時にエラーを報告する。
制約2の場合、このようになる。
制約2:
a
,b
,c
のいずれかの絶対値が100より大きい場合、例外が創出される。(問題3:巨大な係数)
Javaで書き下すと以下のようになる。これも非常に直接的だ。
@Condition public boolean coefficientsAreValid() { return -100 <= a && a <= 100 && -100 <= b && b <= 100 && -100 <= c && c <= 100; }
そして最後の制約、判別式の判定だ。
制約3:
a
,b
,c
がb * b - 4 * c * a >= 0
を満たさない場合、例外が送出される。(問題1:虚数解)
Javaで書くとこう。
@Condition public boolean discriminantIsNonNegative() { int a = this.a; int b = this.b; int c = this.c; return b * b - 4 * c * a >= 0; }
これも単純だ。
判定基準の実装
正常の場合と異常の場合、あるいは特定の制約が満たされなかった場合等でテスト対象に対して期待するべき動作は異なる。
無論if
文で、現在どの条件でテストを実施しているかを書いてみてもいいが、それでは担当者によるむらも生じるし、その判定文自体のバグも心配になる。
JCUnitでは、上で定義にした制約を参照してどのメソッドを用いてテスト対象の動作を検証するべきかを定義できる。 ここでは正常系の場合と、上で定義した各制約のうち制約1の場合だけを説明する。
まず正常系の場合だが、
ということになる。 これをJCUnitで表現すると以下のようになる。
@Test @When({ "*" }) public void solveEquation$thenSolved() { QuadraticEquation.Solutions s = new QuadraticEquation(a, b, c).solve(); assertThat( String.format("(a,b,c)=(%d,%d,%d)", a, b, c), Math.abs(a * s.x1 * s.x1 + b * s.x1 + c), closeTo(0, 0.01) ); assertThat( String.format("(a,b,c)=(%d,%d,%d)", a, b, c), a * s.x2 * s.x2 + b * s.x2 + c, closeTo(0, 0.01) ); }
@When
に指定した({ "*" })
は「@Condition
メソッドがすべて成立(true
を返した)時に実行する」という意味である。
したがって、このメソッドの意図するところは
(When)制約をすべて満たした場合、 方程式を解く、 (Then)すると確かに解ける。
ということになる。
次に異常系だが、
@Test(expected = IllegalArgumentException.class) @When({ "!aIsNonZero" }) public void solveEquation1$thenThrowIllegalArgumentException() { new QuadraticEquation( a, b, c).solve(); }
この部分はaIsNonZero
メソッドがfalse
になったとき、という意味だ。
@When({ "!aIsNonZero" })
そして、JUnitの機能で送出されるべき例外の型を確認している。
@Test(expected = IllegalArgumentException.class)
このメソッド宣言部の意味はこうなる。
(When)
aIsNonZero
制約に違反した場合、 方程式を解く、 (Then)するとIllegalArgumentException
を投げる。
これも見たとおりの意味だ。
実行(その1)
さて、ここまで出来上がったテストを実行するとどうなるか?読者のIDEで実行してみて欲しい。 109件のテストを生成し、6件のみが成功する。
109件にまで増えている理由は、各因子の水準が増えていることによる。実のところ上で「制約」を定義したものの「制約違反を避けるように組み合わせを選ぶ」処理がまだ作られていない。これは次回、解決方法とともに論じたいが「組み合わせテスト」の趣旨から外れるこまった問題だ。 失敗が増えている理由は、例外を送出するべきところ送出する処理がテスト対象にまだ実装されていないからだ。
生成されたテストスイートや実行結果を見てみると、いくつか改善したい点がある。
これらについても次回はもう少し深く議論し、JCUnitでの解決方法を示したいと思う。
余談
今回の記事を書いてみて思ったのだが、@When
アノテーションは@Given
という名前の方がよかったかもしれない。
すると、
@Test(expected = IllegalArgumentException.class) @Given({ "!aIsNonZero" }) public void whenSolveEquation1$thenThrowIllegalArgumentException() {
と書け、これは
(Given)
aIsNonZero
制約に違反している時に (When)方程式を解く (Then)するとIllegalArgumentException
を投げる。
こういう意味になる。これはBDDっぽくってひょっとしてかっこいいのではなかろうか。 読者諸賢のご意見を賜りたいところである。