(第一回)JCUnitで2次方程式を解くプログラムをテストする。
今回からしばらく、JCUnitの使い方を解説するために簡単なプログラムをテストしてみたいと思う。
例題としてQuadraticEquationSolver
(2次方程式ソルバー)を取り上げる。a
,b
,c
の三つの入力をとり、これらを係数とする2次方程式を解の公式を使って解く他愛もないプログラムだ。
準備
0.6.0リリース。 - jcunit's blog を参考に、JCUnitをDependencyに持つMavenプロジェクトを用意して欲しい。
SUT(テスト対象ソフトウェアについて)
Javaで2次方程式を解くプログラムを作るとしたらどんなものだろうか。大体、こんな感じになるのではないか。
public class QuadraticEquationSolver { public static class Solutions { public final double x1; public final double x2; public Solutions(double x1, double x2) { this.x1 = x1; this.x2 = x2; } } private final int a; private final int b; private final int c; public QuadraticEquationSolver(int a, int b, int c) { this.a = a; this.b = b; this.c = c; } public Solutions solve() { double a = this.a; double b = this.b; double c = this.c; return new Solutions( (-b + Math.sqrt(b * b - 4 * c * a)) / (2 * a), (-b - Math.sqrt(b * b - 4 * c * a)) / (2 * a) ); } }
2次方程式を解の公式を使って解いているだけの単純なプログラムだ。
だが、このプログラムをこのまま使おうとすると、色々と問題に気づくはずだ。
a
、b
、c
の値によっては解が虚数になったり、そもそも2次方程式にならなかったりする。
たったこれだけのプログラムを作るだけでも、プログラマは多くの明示的なあるいは暗黙の仮定を置いている。
こういう場合、ソフトウェアエンジニアはどう振る舞うべきか?こうした数々の仮定の明確化は能力の見せ所であろう。
Solution
クラスを拡張し、複素数を取り扱えるようにするか?a == 0
の場合には一次方程式の解をもとめるか?あるいは例外を送出するか?この例では係数であるa
、b
、c
がint
だが、そもそもこれらは整数で良いのか?等々。
このプログラムがこうした入力に対して示すことであろう「不自然な挙動」はバグと言えるか?バグが定義された仕様と実際の挙動の乖離であるとするならば、仕様がなにも定義されていない以上、どんなへんてこな挙動であろうと「設計通り」と言い張ってみてもよいのだが、あまり生産的な態度でもなかろうし、何より見苦しいのでやめて欲しい。
最初のテストプログラム
が、仕様がないならないなりにでもなにかしらテストっぽいことをしてみないと、仕事は進まない。言語化されていない暗黙の仮定を白日の下に晒す。それは思うにテストの重要な価値ではなかろうか。 JCUnitでこのプログラムをテストするなら、おおよそこういう風になる。
@RunWith(JCUnit.class) public class QuadraticEquationSolverTest1 { @FactorField public int a; @FactorField public int b; @FactorField public int c; @Test public void solveEquation() { QuadraticEquationSolver.Solutions s = new QuadraticEquationSolver(a, b, c).solve(); assertThat(a * s.x1 * s.x1 + b * s.x1 + c, is(0.0)); assertThat(a * s.x2 * s.x2 + b * s.x2 + c, is(0.0)); } }
JCUnitは@FactorField
を付したpublic
メンバa
, b
, c
に様々な値を自動的に代入してからsolveEquation
テストメソッドを実行する。
assertThat
とかis
(CoreMatchers
クラスのメソッドがstatic
インポートされている)とかは、Assert.assertEquals
とおよそ同じ意味だ。
solveEquation
メソッドは、QuadraticEquationSolver#solve
メソッドが出力した値を実際に二次方程式に代入して計算し、その結果が0になるかを確かめる。
JCUnitがデフォルトでa
、b
、c
に代入するのは以下の7つの値のうちの一つだ。
{ 1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE }
これらのすべての値の可能なすべての組み合わせとなると7 * 7 * 7 = 343となる。
JCUnitのデフォルトの挙動では任意の2つの係数(因子)の組み合わせ(aとb、bとc、cとa)に関して可能な値(水準)の組み合わせを網羅するテストスイートを生成する。
つまり{a=100,b=-100}
、{b=-100,c=1}
、{c=1,a=100}
のそれぞれはJCUnitが生成するテストスイートによって必ず網羅される。
しかし、{a=100,b=-100,c=1}
が同時に網羅されるかどうかについては保証しない。
このような網羅性を担保するテストスイートは無数にある。がJCUnitが生成するものは53件のテストケースからなるものだ。7水準の因子が2つあれば、7*7で49通りは必要になる。しかしこのテストは3因子使われているにも拘らず、わずかな増加しか見られない。 ちなみに4因子 * 7水準にしてみると60通り、20因子 * 7水準で125通りになる。この辺になるともはや単純に水準を足し合わせていくやり方(7水準 * 20因子 - 19件=121件)と大差がない。
2水準 * 100因子だとJCUnitは18通りのテストケースしか作らない。それでいて上述した特性を満たすテストスイートになるわけだから、興味深いことではなかろうか。(ただし、JCUnitが生成するテストスイートが一番小さいわけではない。PICTなどは同じ性質を満たしながらもっと小さいテストスイートを生成する)
もう一点、JCUnitの特性で注意を促したいことがある。 それは、同じ因子と同じ水準を使った場合、常に同じテストスイートを生成することだ。
テスト対象製品の挙動自体が不安定で同じ条件のテストが成功したり失敗したりするのはある意味やむを得ない。
例えば、内部的にHashSet
を使えば、その要素の列挙において順番は不定になる。
このような場合には「このバグはこのテストケース107番をおよそ10回に1回失敗させる。修正を行い1,000回同じ条件のテストを行ったところ1度も再現しなくなった」という決着のさせ方をすることになる。
だとしたら「テストケース107番は常に{a=100,b=-1,c=-100}
である」のように言えないと非常に不便だ。
テスト対象製品自体の出力が、なにかしらの理由で固定的に予測できない場合はあり得る。例えば我々がHashSet
クラスを自分自身でテストするときなどがそうだ。
だが、テスティングフレームワーク側の事情でテストスイートが不定になるのは避けるべきだと、私は考えるのだ。
さて、では先程のテストクラスを実行するとどうなるか?
53件のテスケースが生成され、39件も失敗するのだ。 これもなかなか興味深いことではなかろうか。
次回以降、ステップ・バイ・ステップでJCUnitの使用方法を解説していきたい。その過程でテスト対象であるQuadraticEquationSolver
のデバッグ、仕様の精緻化を進めていくことになる。
今回の教材はこちら。