学習テストで学ぶJUnit

9/18のJava勉強会で「学習テストで学ぶJUnit」という内容でお話してきました。

2013-09-18 (水) Java勉強会 - TDC - ニュース - 東北デベロッパーズコミュニティ(TDC)

今回も日本オラクル東北支社のセミナールームをお借りしての開催です。いつもありがとうございます。

はじめに

今回の勉強会は参加者の皆さんに手を動かしてもらいながら進めていきます。
まずは環境構築です。今回は参加者全員がEclipseを使用していました。

学習テストとは

学習テストとは、サードパーティのコードを調査し理解するために作成するテストコードのことです。
テスト駆動開発入門」や「Clean Code アジャイルソフトウェア達人の技」に詳しい内容が書かれています。

外部で制作されたソフトウェア用のテストをいつ作成するのか。→パッケージ内の新機能を初めて使用する前に作成する。
(中略)
ジェームズ・ニューカークは学習用のテストを日常的に作成するプロジェクトについて報告した。パッケージの新規リリースが届いたら、最初にそのテストを実行する(そして、必要に応じて修正する)。テストが動作しなければ、アプリケーションもおそらく動作しないので、アプリケーションを実行させる意味がない。テストが動作すれば、いつでもアプリケーションは動作する。

[テスト駆動開発入門 p134-135 学習用のテスト]

サードパーティのコードについて学習するのは大変なことです。サードパーティのコードを統合するのもやはり大変なことです。そしてこれら2つを同時に行うことは2倍大変です。別の方法をとってみてはどうでしょう?実際に開発中の製品コードを使って実験する代わりに、サードパーティのコードを調査し理解するためにテストコードを書くのです。ジム・ニューカークは、こうしたテストを学習テストと呼んでいます。

[Clean Code アジャイルソフトウェア達人の技 p166 境界の調査と学習]

今回の勉強会では、Java SE APIを題材とした学習テストを作成していきます*1

なぜ学習テストを題材にしたのか

これまでTDDBCのTAを経験したりJUnit勉強会の開催をしてみて、初学者がユニットテストを書くというのはかなりハードルが高いことだと感じていました。
実際TDDBC等では、最初のユニットテストをどうやって書いてよいかずっと悩んでいるペアも見受けられたりしました。

今回の勉強会では、参加者8名の内、JUnitを初めて使う方が7名いたので、まずはユニットテストの書き方に慣れてもらうため、学習テストを書いてもらうのが良いのではないかと思い、今回の勉強会の題材としました。

学習テストの例

学習テストの例としてjava.lang.Math#abs(int)メソッドを取り上げます。
学習テストを作成するには、まず仕様の確認ということで Java API ドキュメントを見てみます。

public static int abs(int a)
int 値の絶対値を返します。引数が負でない場合は引数そのものを返します。負のときは、その正負を逆にした値を返します。
引数が Integer.MIN_VALUE の値 (int の最小値) と等しい場合は、結果も同じ値 (負の値) になります。

パラメータ:
a - 絶対値を決定する引数
戻り値:
引数の絶対値。

http://docs.oracle.com/javase/jp/7/api/java/lang/Math.html#abs(int)

上記内容から、学習テストのテスト項目となるものをTODOコメントとして作成してみます。
Mathクラスのテストなので、テストクラス名はMathTestとしました。

package learning;

public class MathTest {
    //TODO absに1を渡すと1を返す
}

ここで、しばし演習タイムです。
仕様を確認しながら、残りのテスト項目をTODOコメントとして挙げてもらいました。

私の方では、 引数(int型)を同値クラスに分割し、その境界値をテスト項目として挙げました。

同値クラス 範囲
正の数 1以上2147483647(Integer.MAX_VALUE)以下
0 0
負の数(最小値を除く) -2147483647以上-1以下
負の最小値 -2147483648(Integer.MIN_VALUE)
package learning;

public class MathTest {
    //TODO absに1を渡すと1を返す
    //TODO absに0を渡すと0を返す
    //TODO absにマイナス1を渡すと1を返す
    //TODO absに2147483647を渡すと2147483647を返す
    //TODO absマイナス2147483648を渡すとマイナス2147483648を返す
    //TODO absマイナス2147483647を渡すと2147483647を返す
}

ここまで出来たら、仕様から読み取った内容が正しいかを検証するため、学習テストを作成して行きます。
また、テスト項目名をそのままテストメソッド名にします。

package learning;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class MathTest {

    @Test
    public void absに1を渡すと1を返す() {
        assertThat(Math.abs(1), is(1));
    }

    //TODO absに0を渡すと0を返す
    //TODO absにマイナス1を渡すと1を返す
    //TODO absに2147483647を渡すと2147483647を返す
    //TODO absマイナス2147483648を渡すとマイナス2147483648を返す
    //TODO absマイナス2147483647を渡すと2147483647を返す
}

assertThatメソッドの第一引数に実測値(actual value)、第二引数に期待値(expected value)を渡します。
Ctrl + 0 でテストを実行すると、無事グリーンになりました。

同様に他のテスト項目もコードにしてみます。

package learning;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class MathTest {

    @Test
    public void absに1を渡すと1を返す() {
        assertThat(Math.abs(1), is(1));
    }

    @Test
    public void absに0を渡すと0を返す() {
        assertThat(Math.abs(0), is(0));
    }

    @Test
    public void absにマイナス1を渡すと1を返す() {
        assertThat(Math.abs(-1), is(1));
    }

    @Test
    public void absに2147483647を渡すと2147483647を返す() {
        assertThat(Math.abs(2147483647), is(2147483647));
    }

    @Test
    public void absマイナス2147483648を渡すとマイナス2147483648を返す() {
        assertThat(Math.abs(-2147483648), is(-2147483648));
    }

    @Test
    public void absマイナス2147483647を渡すと2147483647を返す() {
        assertThat(Math.abs(-2147483647), is(2147483647));
    }
}

ここまで作成すれば java.lang.Math#abs(int) メソッドの仕様が理解出来るので、学習テストとしてはこれで十分かと思います。

演習

次は演習です。今度は java.lang.String#substring(int) メソッドが題材です。
先ほどと同様、仕様を確認します。

public String substring(int beginIndex)

この文字列の部分文字列である新しい文字列を返します。部分文字列は指定されたインデックスで始まり、この文字列の最後までになります。

"unhappy".substring(2) returns "happy"
"Harbison".substring(3) returns "bison"
"emptiness".substring(9) returns "" (an empty string)

パラメータ:
beginIndex - 開始インデックス (この値を含む)。
戻り値:
指定された部分文字列。
例外:
IndexOutOfBoundsException - beginIndex が負の値の場合、あるいはこの String オブジェクトの長さより大きい場合。

http://docs.oracle.com/javase/jp/7/api/java/lang/String.html#substring(int)

これらを踏まえ、先ほどと同様、テスト項目をTODOコメントで作成して行きます。

package learning;

import org.junit.Test;

public class StringTest {
    //TODO 文字列abcdefのsubstringに0を渡すとabcdefを返す
    //TODO 文字列abcdefのsubstringに1を渡すとbcdefを返す
    //TODO 文字列abcdefのsubstringに6を渡すと空文字列を返す
    //TODO 文字列abcdefのsubstringにマイナス1を渡すと例外が発生する
    //TODO 文字列abcdefのsubstringに7を渡すと例外が発生する
    //TODO 空文字列のsubstringに0を渡すと空文字列を返す
}

文字列"abcdef"(文字列長6)を対象として、テスト項目を挙げてみました。

同値クラス 範囲
負の数 -1以下
0以上文字列長以下 0以上6以下
文字列長より大きい 7以上

また、空文字列(文字列長0)についてのテスト項目も入れてみました。
今度は、例外が発生することを検証する必要がありますね。
テストコードに落とし込んでいきます。

package learning;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import org.junit.Test;

public class StringTest {

    @Test
    public void 文字列abcdefのsubstringに0を渡すとabcdefを返す() {
        assertThat("abcdef".substring(0), is("abcdef"));
    }

    @Test
    public void 文字列abcdefのsubstringに1を渡すとbcdefを返す() {
        assertThat("abcdef".substring(1), is("bcdef"));
    }

    @Test
    public void 文字列abcdefのsubstringに6を渡すと空文字列を返す() {
        assertThat("abcdef".substring(6), is(""));
    }

    @Test(expected = StringIndexOutOfBoundsException.class)
    public void 文字列abcdefのsubstringにマイナス1を渡すと例外が発生する() {
        "abcdef".substring(-1);
    }

    @Test(expected = StringIndexOutOfBoundsException.class)
    public void 文字列abcdefのsubstringに7を渡すと例外が発生する() {
        "abcdef".substring(7);
    }

    @Test
    public void 空文字列のsubstringに0を渡すと空文字列を返す() {
        assertThat("".substring(0), is(""));
    }
}

無事全てグリーンとなった所で、今回は時間切れとなりました。
JUnitについては本当に基礎的な使い方しか出来なかったので、次回以降の勉強会でその辺りも触れられたらなと思います。

TDDBC仙台の告知

最後に、10/12に開催される「TDDBC 仙台 the 3rd」の告知を主催者の砂金さんからして頂きました。
私もJavaのTAとして参加予定です。

私自身、TDDを通じて学ぶ所が多かったので、未体験の方には是非参加してほしいです。

TDDBC 仙台 the 3rd - TDDBC | Doorkeeper

参考文献

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

テスト駆動開発入門

テスト駆動開発入門

Clean Code アジャイルソフトウェア達人の技

Clean Code アジャイルソフトウェア達人の技

*1:OracleJava SE APIの学習テストを公開するか、もしくは学習テストをコミット出来る場を提供して、有志が学習テストを追加していけるようにすれば、Java SE APIの正しい使い方が広まるのに一役買うのではないかと思ったりします。