jcunit's blog

JCUnitの開発日誌(ログ)です。"その時点での"JCUnit作者の理解や考え、開発状況を日本語で書きます。

0.6.4リリース

前回のポストで報告したように、JUCnit 0.6.3やそれを使ったアプリケーションはJDK 1.7.0_79ではコンパイルできない。 2016-04-05 - jcunit's blog

これはJDK 1.7.0_80で修正されたjavacの以下のバグが原因だ。

http://bugs.java.com/view_bug.do?bug_id=8013485

このバグを踏んだ場合、以下のコンパイルエラーが生じる。

annotation com.github.dakusui.jcunit.runners.standard.annotations.Checker is missing value for the attribute <clinit>

この問題を回避するため、以下の修正を含むJCUnit 0.6.4をリリースした。

github.com

上のエラーに遭遇したら、こちらを使ってみて欲しい。 以下がmaven coordinate

    <dependency>
      <groupId>com.github.dakusui</groupId>
      <artifactId>jcunit</artifactId>
      <version>[0.6.4,)</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

JDK 1.6.0_45, JDK 1.8.0_25等でで発生しないことは確認してあるが、このバグがどの時点で混入したかは今のところ不明である。 不本意な内容の修正であるが、現在のメインストリームと考えられるJDK 1.7.0でやっと1年前に修正されたバグであり、現実の問題としてJDKのアップデート(すら)が困難な環境にある人々はそれなりに多くいると考えられるので、実施することにした。

JDK 1.7.0_79以前でコンパイルできない。

JDK 1.7.0_80より古いjavacを使っている場合、JCUnit及びそれを使用したライブラリのコンパイルができないことが判明した。ちなみにこれはjavacのバグだ。 これは、確認している限り、JDK 1.6.0_45では起きないし、手元のJDK 8系統でも起きたことがない。 詳報は後日このブログでお知らせするが、annotation関係のエラーが起きた場合には、JDKのアップデートを考慮してほしい。

(第三回)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

  • 入力:三つの整数a,b,c

    • 制約1: aが0の場合、例外が送出される。(問題4:a==0)
    • 制約2: a,b,cのいずれかの絶対値が100より大きい場合、例外が創出される。(問題3:巨大な係数)
    • 制約3: a,b,cb * b - 4 * c * a >= 0を満たさない場合、例外が送出される。(問題1:虚数)
  • 出力:二次方程式a x^2 + b x + c = 0を満たす実数x1x2

    • 出力される解x1またはx2を上述の2次方程式に代入した時、その誤差は0.01を越えない。(問題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,cb * 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の場合だけを説明する。

まず正常系の場合だが、

出力される解x1またはx2を上述の2次方程式に代入した時、その絶対値は0.01未満になる。(問題2:丸め誤差)

ということになる。 これを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件のみが成功する。

f:id:dakusui:20160314074912p:plain

109件にまで増えている理由は、各因子の水準が増えていることによる。実のところ上で「制約」を定義したものの「制約違反を避けるように組み合わせを選ぶ」処理がまだ作られていない。これは次回、解決方法とともに論じたいが「組み合わせテスト」の趣旨から外れるこまった問題だ。 失敗が増えている理由は、例外を送出するべきところ送出する処理がテスト対象にまだ実装されていないからだ。

生成されたテストスイートや実行結果を見てみると、いくつか改善したい点がある。

  1. テストケースが多すぎる
  2. 正常系のテストと異常系のテストが混ざっていてレビューしづらい
  3. 同時に複数の制約に違反するテストケースが多い。この種のテストケースは多くの場合あまり意味がない。3

これらについても次回はもう少し深く議論し、JCUnitでの解決方法を示したいと思う。

余談

今回の記事を書いてみて思ったのだが、@Whenアノテーション@Givenという名前の方がよかったかもしれない。 すると、

  @Test(expected = IllegalArgumentException.class)
  @Given({ "!aIsNonZero" })
  public void whenSolveEquation1$thenThrowIllegalArgumentException() {

と書け、これは

(Given)aIsNonZero制約に違反している時に (When)方程式を解く (Then)するとIllegalArgumentExceptionを投げる。

こういう意味になる。これはBDDっぽくってひょっとしてかっこいいのではなかろうか。 読者諸賢のご意見を賜りたいところである。

参考

(第ニ回)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>

しかし、bInteger.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を満たす実数x1x2

だから当初予定されていた仕様を変更して虚数解をサポートするという判断に至ることだってあるかもしれない。 が、このポストは、JCUnitの使い方のあらましを示すためのものだ。可能な限り当初の仕様を変えない方向で、これらの問題への対応方法を決めていこう。カッコ内には先程述べたバグの分類(製品バグと仕様バグ)に基づいて、それぞれの問題がどちらに当たるかを記した。

  • 問題1:虚数 二次方程式の解の判別式を用い、実根がない組み合わせが与えられた場合例外を送出する。(仕様バグ、製品バグ)
  • 問題2:丸め誤差 根を方程式に代入した時、その計算結果の絶対値が0.01を下回ること。(仕様バグ)
  • 問題3:巨大な係数 a,b,cいずれも絶対値が100以下とし、その範囲外の値が与えられた場合例外を送出する。(仕様バグ、製品バグ)
  • 問題4:a==0 aに0が与えられた場合例外を送出する。(仕様バグ、製品バグ)

問題の修正(1)

これらの問題を修正するにあたっては、まずは仕様書を更新し、しかるのちテスト対象ソフトウェアを修正するという古式ゆかしい手順を踏んでみよう。

仕様書は以下のように修正されることになる。

QuadraticEquationクラス仕様 version 2

  • 入力:三つの整数a,b,c

    • aが0の場合、例外が送出される。(問題4:a==0)
    • a,b,cのいずれかの絶対値が100より大きい場合、例外が創出される。(問題3:巨大な係数)
    • a,b,cb * b - 4 * c * a >= 0を満たさない場合、例外が送出される。(問題1:虚数)
  • 出力:二次方程式a x^2 + b x + c = 0を満たす実数x1x2

    • 出力される解x1またはx2を上述の2次方程式に代入した時、その絶対値は0.01未満になる。(問題2:丸め誤差)

次回は、テストをこの仕様書に基づいてアップデートする。今風にいうなら「テストファースト」ということになるのだろうか。

(第一回)JCUnitで2次方程式を解くプログラムをテストする。

今回からしばらく、JCUnitの使い方を解説するために簡単なプログラムをテストしてみたいと思う。 例題としてQuadraticEquationSolver2次方程式ソルバー)を取り上げる。a,b,cの三つの入力をとり、これらを係数とする2次方程式を解の公式を使って解く他愛もないプログラムだ。

準備

0.6.0リリース。 - jcunit's blog を参考に、JCUnitをDependencyに持つMavenプロジェクトを用意して欲しい。

SUT(テスト対象ソフトウェアについて)

Java2次方程式を解くプログラムを作るとしたらどんなものだろうか。大体、こんな感じになるのではないか。

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次方程式を解の公式を使って解いているだけの単純なプログラムだ。 だが、このプログラムをこのまま使おうとすると、色々と問題に気づくはずだ。 abcの値によっては解が虚数になったり、そもそも2次方程式にならなかったりする。 たったこれだけのプログラムを作るだけでも、プログラマは多くの明示的なあるいは暗黙の仮定を置いている。

こういう場合、ソフトウェアエンジニアはどう振る舞うべきか?こうした数々の仮定の明確化は能力の見せ所であろう。 Solutionクラスを拡張し、複素数を取り扱えるようにするか?a == 0の場合には一次方程式の解をもとめるか?あるいは例外を送出するか?この例では係数であるabcintだが、そもそもこれらは整数で良いのか?等々。

このプログラムがこうした入力に対して示すことであろう「不自然な挙動」はバグと言えるか?バグが定義された仕様と実際の挙動の乖離であるとするならば、仕様がなにも定義されていない以上、どんなへんてこな挙動であろうと「設計通り」と言い張ってみてもよいのだが、あまり生産的な態度でもなかろうし、何より見苦しいのでやめて欲しい。

最初のテストプログラム

が、仕様がないならないなりにでもなにかしらテストっぽいことをしてみないと、仕事は進まない。言語化されていない暗黙の仮定を白日の下に晒す。それは思うにテストの重要な価値ではなかろうか。 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がデフォルトでabcに代入するのは以下の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件も失敗するのだ。 これもなかなか興味深いことではなかろうか。

f:id:dakusui:20160309073955p:plain

次回以降、ステップ・バイ・ステップでJCUnitの使用方法を解説していきたい。その過程でテスト対象であるQuadraticEquationSolverデバッグ、仕様の精緻化を進めていくことになる。

今回の教材はこちら。

github.com

0.6.0リリース。

長い道のりであったが、0.6.0をリリースした。 Maven coordinateは以下のとおり。

    <dependency>
      <groupId>com.github.dakusui</groupId>
      <artifactId>jcunit</artifactId>
      <version>[0.6.0,)</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>

古いJUnitを使っていると実行時にエラーが出るようなので、JUnitについての依存もバージョンを指定するほうがよいようだ。 今回の新機能は以下の通り。

自分で言うのもなんだが、かなり「使える」水準になってきたと思う。 特にネガティブテストの自動生成は便利だ。

ということで、以前見つけた正規表現逆生成ライブラリxegerのテストを作ってみた。こちら

@RunWith(JCUnit.class)
public class XegerTest {
  @FactorField(stringLevels = {
      ".",
      "A",
      "[A]",
      "[AB]",
      "[A-C]"
  })
  public String characterClass1;

  @FactorField(stringLevels = {
      "?",
      "*",
      "+",
      "{1,2}",
      "{1,3}",
      "{2,3}" })
  public String quantifier1;

  @FactorField(stringLevels = {
      "a",
      "[a]",
      "[ab]",
      "[a-c]" })
  public String characterClass2;

  @FactorField(stringLevels = {
      "?",
      "*",
      "+",
      "{1,2}",
      "{1,3}",
      "{2,3}" })
  public String quantifier2;

  /**
   * 0 ... characterClass1
   * 1 ... quantifier1
   * 2 ... characterClass2
   * 3 ... quantifier2
   */
  @FactorField(stringLevels = {
      "{0}{1}{2}{3}",
      " ({0}{1}{2}){3}",
      " ({0}{1}|{2}){3}",
      " (({0}|{2}){1}){3}"
  })
  public String structure;

  @Test
  public void shouldGenerateTextsThatMatchOriginalRegex() {
    String regex = this.composeRegex();
    Xeger generator = new Xeger(regex, new Random(1));
    for (int i = 0; i < 100; i++) {
      String text = generator.generate();
      assertTrue(
          String.format("Generated text '%s' didn't match regex '%s", text, regex),
          Pattern.compile(
              regex
          ).matcher(text).matches());
    }
  }

  @Test
  public void shouldBeRepeatable() throws InterruptedException {
    int tries = 100;
    int seed = 1;
    String regex = this.composeRegex();
    List<String> first = new ArrayList<String>(tries);
    {
      Xeger generator1 = new Xeger(regex, new Random(seed));
      for (int i = 0; i < tries; i++) {
        first.add(generator1.generate());
      }
    }
    TimeUnit.MILLISECONDS.sleep(1);
    List<String> second = new ArrayList<String>(tries);
    {
      Xeger generator2 = new Xeger(regex, new Random(seed));
      for (int i = 0; i < tries; i++) {
        second.add(generator2.generate());
      }
    }
    assertTrue(
        String.format("Xeger didn't return the same result in spite that the same pattern and seed were given:(pattern=%s, seed=%s)", regex, seed),
        first.equals(second));
  }

  String composeRegex() {
    return MessageFormat.format(
        this.structure,
        this.characterClass1, this.quantifier1,
        this.characterClass2, this.quantifier2
    );
  }
}

こんな風にテストを作るといい感じに2つほどバグが見つかる。

また、今回からドキュメントはWikiにまとめることにした。

github.com

機能追加、改善の要望がもしあれば、

Issues · dakusui/jcunit · GitHub

またはこのブログのコメントにでも寄せてほしい。

0.6.0-alpha

JCUnit 0.6.0 alphaをリリースしてみた。 今回の主要な変更は以下の通り。

  1. カバレッジレポーティング機能
  2. FSM(有限状態機械)の入力履歴検査機能 (InteractionHistory)
  3. リファクタリング
  4. 各種プラグインの構造改善
  5. リクエストのあった生成されたテストスイートの再利用機能

MavenのCoordinateは以下の通り。

    <dependency>
      <groupId>com.github.dakusui</groupId>
      <artifactId>jcunit</artifactId>
      <version>0.6.0-alpha</version>
    </dependency>

今回から、ドキュメントはgithub wikiで管理・公開することにした。

Home · dakusui/jcunit Wiki · GitHub

まだリンク切れがあるが、正式公開までに直したい。