(第ニ回)JCUnitで2次方程式を解くプログラムをテストする。
さて、39件も失敗した前回のテストだが、どのテストがどのように失敗しているかつぶさに見ていこう。 失敗するテストは以下の通り。
{ 1, 3, 4, 6, 7, 8, 9, 10,11,12,13,14,15,17,18,19, 24,25,26,27,28, 30,31,32,33,34,35,36,37,38, 40,41,42,43,45,46,48,49,51}
これらが成功していたり、これら以外のものが失敗していたらJCUnitにバグがあることになる。ぜひ知らせて欲しい。
分析
前提として今回のテストで用いたテストメソッドはこうなっている。
@Test public void solveEquation() { QuadraticEquation.Solutions s = new QuadraticEquation(a, b, c).solve(); double v1 = a * s.x1 * s.x1 + b * s.x1 + c, v2 = a * s.x2 * s.x2 + b * s.x2 + c; assertThat(String.format("%d*x1^2+%d*x1+%d=%f {x1=%f}", a, b, c, v1, s.x1), v1, is(0.0)); assertThat(String.format("%d*x2^2+%d*x2+%d=%f {x2=%f}", a, b, c, v2, s.x2), v2, is(0.0)); }
まずテストケース1
java.lang.AssertionError: 1*x1^2+0*x1+100=NaN {x1=NaN} Expected: is <0.0> but: was <NaN>
NaN
は浮動小数で表すことができない数に対して用いる記号であり、この場合は根を方程式で求めようとすると、虚数になってしまう=実根がないのが原因だ。(問題1:虚数解)
テストケース4も同様。
次にテストケース3を見てみる。
java.lang.AssertionError: 1*x1^2+100*x1+-100=0.000000 {x1=0.990195} Expected: is <0.0> but: was <1.4210854715202004E-14>
これは、テストとしては根を代入して式を計算すると0になるのを期待しているのだが、実際には1.42 * 10^-14という幾らかの大きさを持つ値になってしまっていることが原因である。係数が100や−100のように比較的大きいと、実数計算を行った際の誤差も大きくなる。結果として式の計算結果が0にならなくなってしまっている。(問題2:丸め誤差)
テストケース6も似ている。
java.lang.AssertionError: 1*x1^2+-2147483648*x1+1=1.000000 {x1=2147483648.000000} Expected: is <0.0> but: was <1.0>
しかし、b
がInteger.MIN_VALUE
という大きな絶対値の数であるため、誤差も1.0
という大きな値になっている。(問題3:巨大な係数)
次にテストケース9。これもNaN
だ。
java.lang.AssertionError: 0*x1^2+-1*x1+-100=NaN {x1=Infinity} Expected: is <0.0> but: was <NaN>
しかし、問題1:虚数解とは少し事情が違う。a
が0であるので、解の公式の分母が0になっている。浮動小数点数の演算では分母が0の計算を行った場合にもNaN
が生じるのだ。(問題4:a==0)
「バグ」と「仕様」、「ドキュメント」について
上で見てきたいくつかの問題の修正に先立って「バグ」とは何か、「仕様」や「ドキュメント」とはなにかについて少しだけ論じておきたい。
ソフトウェアのバグには2種類あって2種類しか無い。コードが間違っている。つまり通常我々が呼ぶところの「バグ」(単に「バグ」と呼ぶと紛らわしいので今回のケーススタディのなかでは「製品バグ」と呼ぶことにする)。もうひとつはコードがよって立つべき仕様自体が間違っている「仕様バグ」だ。それゆえ仕様やドキュメントがない状況ならば、どんな動きをしようと「仕様です」と言い張ることが一応は可能だ。だが、どこにも書かれていないとしても実際にはプロダクト設計者の意図、関係者に対する約束や信義、暗黙の前提、業界の慣行や平均的技術水準、社会常識、技術者としての矜持等々からあるソフトウェアのある挙動がバグかどうか判断される。言うなれば「こころの中に仕様がある」という場合もある。程度にもよるがそれで良い場合も多々あろう。
さて、仕様のとおりに作られたプログラムがあり、仕様自体に瑕疵が判明した場合、仕様もプログラムも修正することになる。 しかしこれも「製品バグ」と「仕様バグ」の両方が同時に発生したと考えればよい。 所謂「仕様変更」を仕様バグと考えるかどうか。SIer的な伝統では仕様変更はバグではない、区別する必要があるということになる。お金をもらえるかもらえないかに係るから。 しかし、自分たちで作ったソフトウェアを自分たちで運用する世界ではその区別の重要性は低い。 仕様変更と仕様バグの境界はSIer的マネジメントにおいて必要に応じて決めればいいのであって、ひとまず本稿では興味の対象外である。
ところで「仕様バグ」だけが発生することがあるのか? ある。 実装者が仕様の瑕疵に気づき、より適切な実装を行ったものの、仕様書の更新が行われなかった場合や、仕様のある部分を見ずに実装し、仕様と異なっていたが結果的にその実装のほうがより適切と判明した場合、特殊な条件下で一見奇妙な動作に見えるが精密に検討してみると正しいと言わざるを得ない場合、などがこれにあたる。 この場合にはリリースノートの更新や、仕様書の修正が行われることになる。心の中にしか仕様書が無いなら心の中の仕様書を更新することになる。
実際にどのような粒度のドキュメントがどのような抽象度、どのような詳細さで書かれるべきかはこのエントリの範囲では立ち入らない。どういった粒度や抽象度の仕様が適切かはプロダクト、プロジェクト、企業文化、重要なステークホルダー、担当者の好みなど様々な要因で変わる。ある製品の仕様書は通常、その製品が何をでき何ができないかを明らかにする。「あるべき挙動」がチームや製品の状況などに照らして明々白々なときにはドキュメントの量は減ることであろう。
今回の一連のエントリの中で私が「ドキュメント」や「仕様」というとき、それは単に「ソフトウェア製品の言語化されたあるべき動作」という意味である。ものものしく定式化された体系的書類のことだけではない。
問題の整理
では問題を改めて整理してみよう。 「分析」の節で上がった問題は以下の4つ。
これらが昔風にいうならバグ表の項目、今どきならJIRAのチケットに対応する。
これらをどういう形で決着させるか。実に色々な形があり得る。なぜならばここまでのところ、我々のQuadraticEquation
クラスの仕様は以下のものでしかないので様々な側面を明確にする余地があるし、その仕様にしたって正当な理由があれば適切な手順を踏んで仕様を変えることもありえるのがソフトウェア開発というものだ。
QuadraticEquationクラス仕様 version 1
入力:三つの整数
a
,b
,c
出力:二次方程式、
a x^2 + b x + c = 0
を満たす実数x1
とx2
だから当初予定されていた仕様を変更して虚数解をサポートするという判断に至ることだってあるかもしれない。 が、このポストは、JCUnitの使い方のあらましを示すためのものだ。可能な限り当初の仕様を変えない方向で、これらの問題への対応方法を決めていこう。カッコ内には先程述べたバグの分類(製品バグと仕様バグ)に基づいて、それぞれの問題がどちらに当たるかを記した。
- 問題1:虚数解 二次方程式の解の判別式を用い、実根がない組み合わせが与えられた場合例外を送出する。(仕様バグ、製品バグ)
- 問題2:丸め誤差 根を方程式に代入した時、その計算結果の絶対値が0.01を下回ること。(仕様バグ)
- 問題3:巨大な係数
a
,b
,c
いずれも絶対値が100以下とし、その範囲外の値が与えられた場合例外を送出する。(仕様バグ、製品バグ) - 問題4:a==0
a
に0が与えられた場合例外を送出する。(仕様バグ、製品バグ)
問題の修正(1)
これらの問題を修正するにあたっては、まずは仕様書を更新し、しかるのちテスト対象ソフトウェアを修正するという古式ゆかしい手順を踏んでみよう。
仕様書は以下のように修正されることになる。
QuadraticEquationクラス仕様 version 2