2011年10月10日月曜日

自動テストのレベル分け

最近は、「xUnit で UnitTest を書いてカバレッジとってます」なんて、どいつもこいつも謳ってるけど、お前らマジかと。それで出来てるつもりなのかと…?

というわけで、秋の朝がさわやかなので、自動テストの出来てる度合いの段階区分を考えてみたい。

  Level 3: 出来てる:常時テスト成功、高カバレッジ

ちゃんとできてるプロジェクトの出来る子ちゃん達からしたら、取り立てて言及することもない当たり前の状態だと思う。

"Clean Check-In" が普通に守られているから、当然、テストも常時全件成功する。まあ基本中の基本。

で、TDD/TestFirst で開発してるから、開発の最初期から高カバレッジ(計測対象範囲内でのカバレッジ基準充足)で始まり、進捗に伴いソースコード量が増えていく中でも、高カバレッジを維持したまま推移する。


Level 2:怪しい:ほぼ常時テスト成功、低カバレッジ

"常時"かつ"全件"、テストが成功してはいるけど、カバレッジが低いプロジェクト。まあ、後付けでテストを書こうとすると、えてしてそんな感じになる。

原因としては、単にスキルがないから仕方なく後付けになったり、確信的にテストをサボっていたり、いろいろあるにせよ、結果的に以下のような感じになる。

①:テスト・コーディングに凄い余計な時間が掛かる。テスト・ファーストでやってさえいれば自然に得られたはずの保守性・メンテ性が備わってない低テスタビリティ・コードに無理やりテストコードを書いてく事になるから、まともな生産性は無理。

② :テスティング・ポイントがウヤムヤになる。本体コードを書いていた時点では脳内にあったはずのコーディング意図がとっくに揮発した状態で、テストコードを後付けする破目になる。何をテストしてるのか本人が分かってない状態。

③ :テストコードがザルになる。①で指摘した生産性の低いテストコードを、②で指摘した曖昧状態のプログラマが書いてくわけだから、「何かをテストする」事ではなくカバレッジを通す事が自己目的化してしまい、結果、Assertion も Expectation も不十分で、単に実行経路に含まれただけの空虚なテストコードになってしまう。

まあ、後付けテストの全てがそんな糞テストコードばかりとは限らないけど、テストコードは本体コードより先に書いとくに越したことはない。それが普通なのだと認識するだけで、悪い事がいろいろ避けられて良い事がいろいろ増えてくる。


Level 1:下手糞:常時テスト失敗、低カバレッジ

低カバレッジでも、取り敢えずテストが存在して差し当たり成功していれば、まだ見どころはある。また、たまたま間違えてテストを壊す事もあると思うが、そんなのすぐ直せば良い事で別に問題じゃない。

だけど、テストが通らないコードをコミットするのが常態化していたり、CIサーバでテストが失敗してるのに放置されていたりしたら、それはかなりの低スキル・チームの疑いがある。

実は、そうしたプロジェクトでは、Level 3 の状態を志してはいたもののスキル不足で健闘虚しくって感じじゃなくて、 Level 1 の状態で普通・正常だと思ってる開発者が大勢を占めていたりする。酷いのになると「今までに経験したプロジェクトではそれが普通だったし、テストが壊れても気にしない方がリファクタしやすい。」なんて事を真顔で主張したりする(リファクタするためにこそテストコードが必要だという最低限の常識を弁えていれば、そうした発言は出ないんだけど…)。

そうやって考えてくと、そもそも技術者の○○使用経験とか○○歴とかって何なのだろうという問題に突き当たるが、これは別の機会に考えてみたい。あと、Level 1 で生じている問題には、余りにも劣化した形で普及し実践されている『リファクタリング』もあるのだけど、これも別の機会に考える。

朝から、気が滅入ってきた。山でも歩いてこよっと。

2011年10月9日日曜日

Eclipse から XSLT 2.0 を使うやり方

今更だけど、Eclipse の XSL Developer Tool で XSLT2.0 を使うやり方が分かったのでメモっとく。
  • まず、Saxon home edition の新しいやつ (saxonhe9-3-0-8j.zip)をダウンロード。
  • ダウンロードしたZIPを適当なとこに展開して、中の saxon9he.jar を出しておく。
  • Eclipse のメニューを [Window]>[Preference]>[XML]>[XSL]>[Java Processors] ってたどる。
  • Add を押して開いたダイアログボックスで、だいたい以下の様な感じで入力。追加したやつのチェックボックスを選択しておく。
これで使えるようになる。 試しに、昔のポストで書いた XSLT で平均と分散を計算するやつを、XSLT2.0 を使ってやってみる。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes" />
  <xsl:template match="/list">
    <xsl:variable name="result">
      <xsl:call-template name="stat">
        <xsl:with-param name="items" select="item" />
        <xsl:with-param name="subtotal" select="0" />
        <xsl:with-param name="count" select="0" />
      </xsl:call-template>
    </xsl:variable>
    <average>
      <xsl:value-of select="$result/avg" />
    </average>
    <varianece>
      <xsl:value-of select="$result/sumOfD2 div $result/count" />
    </varianece>
  </xsl:template>
  <xsl:template name="stat">
    <xsl:param name="items"/>
    <xsl:param name="subtotal"/>
    <xsl:param name="count"/>
    <xsl:choose>
      <xsl:when test="$items">
        <xsl:variable name="t">
          <xsl:call-template name="stat">
            <xsl:with-param name="items" select="$items[position() > 1]" />
            <xsl:with-param name="subtotal" select="$subtotal + $items[1]" />
            <xsl:with-param name="count" select="$count + 1" />
          </xsl:call-template>
        </xsl:variable>
        <sumOfD2><xsl:value-of select="($items[1]-$t/avg)*($items[1]-$t/avg)+$t/sumOfD2"/></sumOfD2>
        <count><xsl:value-of select="$t/count"/></count>
        <avg><xsl:value-of select="$t/avg"/></avg>
      </xsl:when>
      <xsl:otherwise>
        <sumOfD2>0</sumOfD2>
        <count><xsl:value-of select="$count"/></count>
        <avg><xsl:value-of select="$subtotal div $count"/></avg>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

前回と同じ入力XMLから同じ結果が得られることが確認できた。

前にやった時は、結果ツリーフラグメントの制約から template の結果を文字列にして返さざるを得なくて煩わしかったけど、XSLT 2.0 の temporary tree では 普通の XML データとして扱えるので、やってる事をより直截に表現したコードになった。

2011年9月29日木曜日

リファクタリング本の Cyclomatic complexity

『リファクタリング』 を読んだ人なら、以下のコードに見覚えがあるかもしれない。第一章「最初の例」でのリファクタリング実演のために用意された架空の「長すぎるメソッド(Long method)」で、このメソッドを含む一群のコードを改善する過程で、リファクタ術が披露される。

まあ、実演のためのサンプルだから、本気でバカ長いメソッドを掲載するわけにはいかないだろうけど、それでも全然簡潔ではないし見た目も悪い。

自分なら普通、こういうのを製品コードに残したりはしないけど、それは単に自分にとって綺麗とか汚いといった審美的な感覚だけではなくて、他の開発者のミスリーディングを引き起こしたり生産性を下げたりといった、つまり他人様に迷惑をかける真似はすべきでないという、割と真面目というか普通な社会人的常識からでもあったりする。

  public String statement() {
      double totalAmount = 0;
      int frequentRenterPoints = 0;
      Enumeration rentals = _rentals .elements();
      String result = "Rental Record for " + getName() + "\n";
      while (rentals.hasMoreElements()) {
         double thisAmount = 0;
         Rental each = (Rental) rentals.nextElement();
         
         switch (each.getMovie().getPriceCode()) {
         case Movie.REGULAR:
            thisAmount += 2;
            if (each.getDaysRented() > 2)
               thisAmount += (each.getDaysRented() - 2) * 1.5;
            break;
         case Movie.NEW_RELEASE:
            thisAmount += each.getDaysRented() * 3;
            break;
         case Movie.CHILDRENS:
            thisAmount += 1.5;
            if (each.getDaysRented() > 3)
               thisAmount += (each.getDaysRented()  - 3) * 1.5;
            break;
         }
         frequentRenterPoints ++;
         if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
               each.getDaysRented() > 1) frequentRenterPoints ++;
         
         result += "\t" + each.getMovie().getTitle() + "\t" +
         String.valueOf(thisAmount) + "\n";
         totalAmount += thisAmount;
      }
      result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
      result += "You earned " + String.valueOf(frequentRenterPoints) +
      " frequent renter points";
      
      return result;
   }

ところで、このコードの扱いにくさを「計測」するとどうなるか。よく使われている尺度としては、Cyclomatic Complexity があるが、どんなもんだろう。

ちなみに、CheckStyle の解説では、

  • 1-4: considered good
  • 5-7: ok
  • 8-10: consider re-factoring
  • 11+: re-factor now!
とある。

で、実際に測ってみると出てきた複雑度は 9 だった。"consider re-factoring"という事になるから、本の中での実演のネタとしては、正に丁度良かったという事になる。

さらに本に従って最後の段階までリファクタを施したコードの CyclomaticComplexity を測ってみると、結局 2 にまで低減していた。さすがにこれくらいまでやると、変な引け目も後ろめたさもなくソース・コードを共有できる。

で、せっかくある程度客観的にコードの良し悪しがでるのだから、自分だけじゃなくチームにも、簡潔で扱いやすいコードを書く方向で前向きに頑張ってほしいと思うのだけど、いろんな人が集まる現実のプロジェクトではそうも言ってられない場合も多い。

特に、すでにある程度コードが書き貯まったプロジェクトで、途中で CyclomaticComplexity チェックを適用したりなんかすると、「11+」どころか、妥協して15以上とかで設定しても警告が大量発生して、結局「今回は諦めようぜ」って感じになる。

人生って厳しいよね。

2011年9月18日日曜日

依存 jar を変更したらどうなるか

あるアプリケーションがビルド時に参照していた ライブラリの jar を変更して、そのアプリケーション再コンパイルせずに実行したらどうなるか。

====
以下のようなライブラリ・コードを書き、コンパイルして foo.jar に丸めておく。
package p1;

public class Foo {
  public String bar() {
    return "hello";
  }
}
で、このライブラリを使う以下のようなクライアントコードを書いて、-cp に foo.jar を指定してコンパイルする。
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(new Foo().bar());
  }
}

コンパイルされたクラスを java コマンドで -cp に foo.jar を指定して実行すると、標準出力に hello と出力される。


さて、ここで クラス Foo を変更して、つまり foo.jar の中身を変えて、再コンパイルせずに再度 Client#main() を実行したらどうなるか。

(1) メソッドの中身を変えてみる

  public String bar() {
    return "good-bye";
  }
問題無し。標準出力に good-bye と表示される。

(2) メソッドの名前を変えてみる

  public String Bar() {
    return "hello";
  }
メソッド p1.Foo.bar()Ljava/lang/String が見当たらないって事で、NoSuchMethodError が送出される。リフレクションのコーディングでよく catch したりするNoSuchMethodExceptionではなく、NoSuchMethodError が投げられてきた。

(3) メソッドの引数を変えてみる

  public String Bar(String s) {
    return "hello";
  }
これは (2) と同じ

(4) メソッドの戻り値を変えてみる

  public Object Bar() {
    return "hello";
  }
これも (2) と同じ。戻り値も識別される。

(5) throws を追加してみる

  public String bar() throws IOException {
    throw new IOException("test");
  }
これは意外にも普通に実行されて、IOExceptionが送出されてスタックトレースされる。意外というのは、これを再コンパイルしようとすると、コンパイルエラーが出るからで、Client#main() に throws を追加するか、bar() を try-catch で囲むかしないと、コンパイルが通らない。でも、コンパイルは通らなくても実行時のメソッド呼び出しは成功する。

(6) メソッドの可視性を変えてみる

  private String bar() {
    return "hello";
  }
これは IllegalAccessError が送出される。つまり、一応 p1.Foo.bar()Ljava/lang/String の存在は識別された上で、アクセスに失敗して例外が発生した模様。

(7) クラスの可視性を変えてみる

class Foo {
…
(6) と同様だが、クラス p1.Foo の存在を識別したあとに、アクセスするところで失敗している。

(8) 定数を変えてみる

メソッドは以上のような感じで、フィールドもだいたい想像がつく。で、ここでちょっと定数を試してみる事にする。

まずライブラリコードを以下のように変える。

package p1;

public class Foo {
  public static final int C = 100;
}
次に、クライアントコードを以下の様に変える
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(Foo.C);
  }
}

両方コンパイルして実行すると、標準出力に100が実行される。

ここでライブラリコードを以下の様に修正し、jar を作り直す。

  public static final int C = 200; 
で、クライアントコードを再実行すると、標準出力に 200 が出力されると思いきや、実際には変更前と同じ100が出力される。
javap で見てみると、バイトコードに定数 100 が埋め込まれているのがわかる。なるほど定数はこういう扱いらしい。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: bipush 100
   5: invokevirtual #3; //Method java/io/PrintStream.println:(I)V
   8: return 

(9) null値 を試してみる

ライブラリ・コード を以下のように変更して、両方とも再コンパイルして実行してみる。
package p1;

public class Foo {
  public static final String C = null;
}
標準出力には、null が出力される。

ここでライブラリ・コードを、public static final String C = "a"; に変更して、クライアントを実行してみる。

(8) の結果から、null が出力される結果を類推してしまうが、実際には "a" が出力される。理由は、Java 言語仕様として null 定数として扱われないためコンパイル時には byte コードに直接書き込まれず、実行時に初めて Foo.Cの値を読むことになるかららしい。javap は以下のようになる。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: getstatic #3; //Field p1/Foo.C:Ljava/lang/String;
   6: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   9: return

2011年8月27日土曜日

Advanced Level テストマネージャ試験受けてみた

今日、JSTQB という団体がやってる、ソフトウェア・テスト技術者試験の Advanced Level 試験(Foundation Levelの上級試験)を受けてきた。

全65問のうち、きっと問題のミスだろうと思われる不可解なところを除けば、全問できたような気がする。まあ9割5分とか9割でも普通は合格だと思う。

というかあまり難しくなかった。去年、実施したらしいトライアル版試験の統計をみると合格率10%というから、さぞかし難関だろうと思ってたけど、試験時間を半分過ぎた辺りから余裕な感じで退出する人も多かったりして、今回は合格率50%を越えるような気がしないでもない。

ところで、試験勉強はISTQB(JSTQBの親の国際団体)が出している試験のシラバス(学習事項)をひたすら読むだけだけど、これがよくできていて勉強が苦にならない。

以前のポストで、Foundation Level のシラバスを紹介したけど、さすがにそれよりはテスター向けの専門性が高い。それでも自分のようなテスト専門じゃない開発者にとっても、読み応えがある。

実プロジェクトのテスト技術者の教科書としてもそのまま使えそうな内容で、暗記事項の羅列などはほとんど無いかわりに、なんというか現場感覚があるので、過去に携わったプロジェクトや今現在進行中のプロジェクトについて、いろいろ考えさせられる。

ちなみに Advanced Level 資格には テストマネージャ、テストアナリスト、テストテクニカルアナリストという種別があり(今回のはテストマネージャだった)、3つそろったら、なんか格上の称号が与えられるらしい。まあ、欲しくないと言えば嘘になる。

2011年8月15日月曜日

JMockit: 基底クラスのコンストラクタのすりかえ

基底クラスのコンストラクタ呼び出しだけをモックですりかえるにはどうすれば良いか。

以下のように実験してみた。AssertionError を投げているところを、設定ファイルの読み込みとかマスタデータへの依存とか、面倒なセットアップが必要だったり時間がかかったりする処理に読み替えると、ニーズが分かると思う。

====
例題 (1)
クラス BaseClass を継承したクラス CuT があるとする。こんな感じ。
public class CuT extends BaseClass {

public CuT() {
super();
}
}

ところが、BaseClass の実装はこんな感じだったとする。
public class BaseClass {

protected BaseClass() {
throw new AssertionError();
}
}

以下のテストコードを実行すると、当然、AssertionEror が発生してテストが落ちるが、落とさずにインスタンスを作るにはどうすれば良いか?
@Test public void testCuTConstructor() {

//ここに何を書けば落ちないか
new CuT();
}

答え (1)
以下の様にして、基底クラスのコンストラクタをすり替えられる。
@Test public void $init() {

new MockUp<BaseClass>() {
@Mock void $init() { /*とりあえず何もしない*/}
};
new CuT();
}


例題(2)
例題(1) にちょっと書き足してみる。
public class BaseClass {

protected final int bar;
protected BaseClass(int bar) {
this.bar = bar;
throw new AssertionError();
}
}
public class CuT extends BaseClass {
public CuT(int bar) {
super(bar);
}
public int foo() {
return this.bar * 100;
}
}
@Test public void $init2() {
new MockUp<BaseClass>() {
//ここに何も書かないと、下のアサーションが失敗する
};
Assert.assertEquals(200, new CuT(2).foo());
}
このままだと基底クラスの bar が 2 で初期化されず、foo() が 0 * 100 = 0を返してしまうので、アサーションが失敗する。MockUp の匿名インナー型定義に、何を書き足せばテストが通るか?


答え (2)
コンストラクタ実行中のタイミングで、モック対象インスタンスの bar フィールドに適当な値を設定すれば良いわけだが、そのインスタンスをどう取得するかというのが問題。"it"フィールドという仕組みを利用すれば良いらしい。
@Test public void $init2() {

new MockUp<BaseClass>() {
BaseClass it;
@Mock void $init(int bar) {
Deencapsulation.setField(it, bar);
}
};
Assert.assertEquals(200, new CuT(2).foo());
}
この例では、bar は private フィールドなのでDeencapsulation ユーティリティクラスを使っている。ちなみに bar は final だけど、問題なく設定できている。

2011年8月14日日曜日

JMockit を使って簡単に「後回し」するやり方

二つのクラスがあって、片方がもう片方に依存しているとする。つまり、参照関係や使用関係がある。

両方とも開発対象の場合、常に「依存される」側からしか作ろうとしない/作れない開発者がいるけど、逆の方向、つまり「依存される」側を一旦後回しにして「依存する」側から作った方が見通しが良い場合が多い。

方法はいくつもあるが、今日は JMockit を使って実演してみる。

====
◆ 仕様
依存する側のクラス
クラス CuT:Collaborator を使うクラス。こいつから作る。
 関連オブジェクト:
  col1: Collaboratorのインスタンス
  col2: Collaboratorのインスタンス
 コンストラクタ:受け取った2つの整数によりcol1 と col2 を初期化する
 メソッド:
  int foo(int number):
   col1 と col2 の bar() に number を渡し、結果の合計を返す

依存される側のクラス
Collaborator:CuT に使われるクラス。後回しにする。
 コンストラクタ:受け取った整数を保持する
 メソッド:
  int bar(int number)
   コンストラクタで受け取った整数と引数 number を用いて、何か計算をして返す。


◆ コーディング
両クラスのガラだけ書いてみる。CuT から作ってく趣旨なので、テストクラス CuTTest も書き始める。
public class CuT {}

public class Collaborator {}
public class CuTTest {}

まず、コンストラクタのテストを CuTTest に追加する。CuT のコンストラクタで受け取った引数が、二つの Collaborator のコンストラクタに受け渡されれば良いので、次のようなテストメソッドを書く。
@Test public void $init(final Collaborator mock) {

new Expectations() {
{
new Collaborator(2);
new Collaborator(3);
}
};
new CuT(2, 3);
}

とりあえず、存在しないコンストラクタを呼んでいるのでコンパイルエラーになるから、これを解消する。

まず、Collaborator のコンストラクタだが、こいつは以下の様なコードで後回しにする。
public Collaborator(int i) {

throw new AssertionError();
}

次に、CuT のコンストラクタだが、まずモックがちゃんと効いているか確かめるために、以下のように空の実装を書いて、一度テストを実行してみる。
public CuT(int number1, int number2) {}
テスト実行すると、「コンストラクタが引数 2 で呼ばれていない」といった感じのエラーメッセージが出力されるので、今度は以下のような感じで、ちゃんと書く。
public class CuT {

final Collaborator col1;
final Collaborator col2;
public CuT(int number1, int number2) {
this.col1 = new Collaborator(number1);
this.col2 = new Collaborator(number2);
}
}
再度テスト実行すると、テスト成功。

続いて同様に、まずテストから foo() を書く。

テスティングポイントは、「foo() で受け取った引数を、col1, col2 の bar()に渡している事」と「col1, col2 の bar()の戻り値の合計を、foo() の戻り値とする」なので、以下のようなテストコードになる。
@Test public void foo(

@Mocked(capture=1) final Collaborator col1,
@Mocked(capture=1) final Collaborator col2) {
new Expectations() {{
col1.bar(7); result = 10;
col2.bar(7); result = 100;
}};
Assert.assertEquals(110, new CuT(2, 3).foo(7));
}
Collaborator#bar() の実装が無いので、コンストラクタ同様に AssertionError 送出のみの後回しコードを追加して、コンパイルエラーを解消する。
public int bar(int i) {

throw new AssertionError();
}

CuT#foo() は、とりあえずダミーの固定値を返すようにして、テストを実行してみる。
public int foo(int i) {

return -1;
}

実行すると「bar が引数7 で呼ばれていない」といったエラーメッセージが出力される。期待通りなので、おもむろに foo() の実装を本物コードに修正する。
public int foo(int i) {

return this.col1.bar(i) + this.col2.bar(i);
}
これで、テストがちゃんと通るようになる。

一段落ついたら、クラス CuT の事は忘れて、Collaborator 実装に集中して取りかかればいい。

◆ 補足
ちなみに、JMockit に不慣れで capture の意味が分からなければ、このサンプルの col1 と col2 の capture を変えてテスト実行すれば、capture がどういう働きを持つのか分かると思う(場合によっては Expectations を NonSrictExpectations に替える必要がある)。

例えば、col1 のcapture を2に変えると、foo()の戻り値は20になる。col2 をそのまま変えずに col1 の capture を 0 にすると、foo()の戻り値は100になり、col2 の capture を2にすると、foo()の戻り値は200になる。

テストメソッド CuTTest#foo() のパラメータの col1, col2 が、CuT のフィールドのcol1, col2 のどれに対応するかが、capture で制御されているのが分かると思う。

さらに、もう一つ追記。上の CuTTest#foo()は以下のように書き換えられる。
@Test public void foo_explicitVerification(

@Mocked(capture=1) final Collaborator col1,
@Mocked(capture=1) final Collaborator col2) {
new NonStrictExpectations() {{
col1.bar(anyInt); result = 10;
col2.bar(anyInt); result = 100;
}};
Assert.assertEquals(110, new CuT(2, 3).foo(7));
new Verifications() {{
col1.bar(7);
col2.bar(7);
}};
}

若干、冗長になるが、テストの都合で指定したい動作と、検証したい動作を分けて書いている。実際の業務ロジックなんかで複雑な相互作用がある場合など、Expectations に全部書いてしまうと何がテスティングポイントだったのか分かりにくくなる事がある(特に後付けでテストを書かざるを得ない状況などで)。そんなとき Verifications のブロックに本当に検証すべきだった事を書いておくと、テスティングポイントを見失わずにすむ。

ちなみに、Verifications には、FullVerifications, FullVerificationsInOrder, VerificationsInOrderといった派生クラスがあるから、適当に使い分けるべし。