JUnit4 のパラメータ化テストは4通りの方法で書ける

公開日: : 最終更新日:2015/10/10 JUnit4 ,

JUnit 本である「JUnit実践入門」でテストの書き方を勉強しています。
本書でも紹介されている「パラメータ化テスト」は4通りの方法で書けるようなのでメモしておきます。

目次

1. 4通りの書き方について

調べたところ、パラメータ化テストは4通りの方法で書けるようです。
下にサンプルを載せますが、まずはその4つのタイプについて書いておきます。

  • パターン1:Theories テストランナーと @DataPoint / @DataPoints アノテーションを使う
  • パターン2:Fixture を使う
  • パターン3:Parameterized テストランナーを使う
  • パターン4:@ParametersSuppliedBy アノテーションを使う

1-1. パターン1:Theories テストランナーと @DataPoint / @DataPoints アノテーションを使う

おそらくパラメータ化テストの基本形だと思われるものです。
@RunWith(Theories.class)@DataPoint / @DataPoints アノテーションとテストメソッドに @Theory アノテーションを付与するパターンです。

パターン2:Fixture を使う

@DataPoint / @DataPoints アノテーションを付与するパラメータを「フィクスチャオブジェクト」に置き換えるものです。
フィクスチャオブジェクトとは、JUnit実践入門には次のようにあります。

JUnit で可読性の高いパラメータ化テストを行うポイントは、引数を1つとすることです。(略)パラメータ群を1つのオブジェクトにまとめると可読性の高いコードとなります。

パターン3:Parameterized テストランナーを使う

Theories テストランナーではなく Parameterized テストランナーを使うというものです。
この Parameterized テストランナーですが、JUnit実践入門によると次のようにあり、今後使われなくなっていく運命にあるかもしれません。
なので、ここでも Parameterized テストランナーを使ったサンプルは省略します。

歴史的には、Parameterized テストランナーのほうが先に実装された機能です。しかし、ほかの xUnit 系フレームワークではテストメソッドでパラメータを受け取るしくみが一般的であるため、今後は Theories テストランナーを使ったほうがよいでしょう。

パターン4:@ParametersSuppliedBy アノテーションを使う

パラメータ化テストについて調べているときに発見したものです。
個人的にはあまり使い勝手が良いとは思っていないのですが、せっかくなのでサンプルを載せることにしました。

2. サンプルの動作環境

動作環境は次の通りです。
まずは Java のバージョン。

$ java -version
java version "1.7.0_11"
Java(TM) SE Runtime Environment (build 1.7.0_11-b21)
Java HotSpot(TM) 64-Bit Server VM (build 23.6-b04, mixed mode)

JUnit は2013年8月26日時点での最新である 4.11 を使いました。
JAR のダウンロードですが、Maven であれば次の dependency を pom.xml に貼り付けます。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
</dependency>

そうしたら、次のコマンドを実行すればローカルリポジトリに JAR がダウンロードされます。

$ mvn eclipse:eclipse -DdownloadSources=true

また Maven を使わずに JAR をダウンロードして直接プロジェクトに組み込む場合は次のサイトからダウンロードできます。
ダウンロードページの URL は次になります。

3. 動作確認に使ったテスト対象クラス

動作確認用のクラスはJUnit実践入門のP381にある例題「境界値のテスト」を使用しました。

public class Range {

    public final double min;
    public final double max;

    public Range(double min, double max) {
        this.min = min;
        this.max = max;
    }

    public boolean contains(double d) {
        if(d < min || max < d) {
            return false;
        }
        return true;
    }
}

4. サンプルテストクラス

4-1. パターン1:Theories テストランナーと @DataPoint / @DataPoints アノテーションを使う

パターン1のテストクラスのサンプルは次の通りです。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.experimental.runners.Enclosed;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class RangeTestWithParameterized {

    @RunWith(Enclosed.class)
    public static class Rangeがマイナス5_1から5_1で {

        static Range sut = new Range(-5.1, 5.1);

        @RunWith(Theories.class)
        public static class 範囲外の場合 {

            @DataPoints
            public static double[] VALUES = {-5.2, 5.2};

            @Theory
            public void containsはfalseを返す(double value) throws Exception {
                // Verify
                assertThat("values=" + value, sut.contains(value), is(false)); 
            }       
        }

        @RunWith(Theories.class)
        public static class 範囲内の場合 {

            @DataPoints
            public static double[] VALUES = {-5.1, 5.1};

            @Theory
            public void containsはtrueを返す(double value) throws Exception {
                // Verify
                assertThat("values=" + value, sut.contains(value), is(true)); 
            }
        }
    }

    @RunWith(Enclosed.class)
    public static class Rangeが0から10_5で {

        static Range sut = new Range(0.0, 10.5);

        @RunWith(Theories.class)
        public static class 範囲外の場合 {

            @DataPoints
            public static double[] VALUES = {-0.1, 10.6};

            @Theory
            public void containsはfalseを返す(double value) throws Exception {
                // Verify
                assertThat("values=" + value, sut.contains(value), is(false)); 
            }
        }

        @RunWith(Theories.class)
        public static class 範囲内の場合 {

            @DataPoints
            public static double[] VALUES = {0.0, 10.5};

            @Theory
            public void containsはtrueを返す(double value) throws Exception {
                // Verify
                assertThat("values=" + value, sut.contains(value), is(true)); 
            }
        }
    }

}

4-2. パターン2:Fixture を使う

パターン2のテストクラスのサンプルは次の通りです。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.experimental.runners.Enclosed;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class RangeTestWithFixture {

    @RunWith(Enclosed.class)
    public static class Rangeがマイナス5_1から5_1で {

        static Range sut = new Range(-5.1, 5.1);

        @RunWith(Theories.class)
        public static class 範囲外の場合 {

            @DataPoints
            public static Fixture[] FIXTURES = {
                new Fixture(-5.2, false),
                new Fixture(5.2, false)
            };

            @Theory
            public void containsはfalseを返す(Fixture fixture) throws Exception {
                // Verify
                assertThat(fixture.toString(), sut.contains(fixture.value), is(fixture.expected)); 
            }       
        }

        @RunWith(Theories.class)
        public static class 範囲内の場合 {

            @DataPoints
            public static Fixture[] FIXTURES = {
                new Fixture(-5.1, true),
                new Fixture(5.1, true)
            };

            @Theory
            public void containsはtrueを返す(Fixture fixture) throws Exception {
                // Verify
                assertThat(fixture.toString(), sut.contains(fixture.value), is(fixture.expected)); 
            }
        }
    }

    @RunWith(Enclosed.class)
    public static class Rangeが0から10_5で {

        static Range sut = new Range(0.0, 10.5);

        @RunWith(Theories.class)
        public static class かつ範囲外の場合 {

            @DataPoints
            public static Fixture[] FIXTURES = {
                new Fixture(-0.1, false),
                new Fixture(10.6, false)
            };

            @Theory
            public void containsはfalseを返す(Fixture fixture) throws Exception {
                // Verify
                assertThat(fixture.toString(), sut.contains(fixture.value), is(fixture.expected)); 
            }
        }

        @RunWith(Theories.class)
        public static class かつ範囲内の場合 {

            @DataPoints
            public static Fixture[] FIXTURES = {
                new Fixture(0.0, true),
                new Fixture(10.5, true)
            };

            @Theory
            public void containsはtrueを返す(Fixture fixture) throws Exception {
                // Verify
                assertThat(fixture.toString(), sut.contains(fixture.value), is(fixture.expected)); 
            }
        }
    }

    static class Fixture {
        double value; // テストデータ
        boolean expected; // 期待値

        Fixture(double value, boolean expected) {
            this.value = value;
            this.expected = expected;
        }

        @Override
        public String toString() {
            return String.format("when value=%s, expected=%s", value, expected);
        }
    }

}

4-3. パターン4:@ParametersSuppliedBy アノテーションを使う

パターン4のテストクラスのサンプルは次の通りです。

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import java.util.Arrays;
import java.util.List;

import org.junit.Ignore;
import org.junit.experimental.runners.Enclosed;
import org.junit.experimental.theories.ParameterSignature;
import org.junit.experimental.theories.ParameterSupplier;
import org.junit.experimental.theories.ParametersSuppliedBy;
import org.junit.experimental.theories.PotentialAssignment;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class RangeTestWithParametersSuppliedBy {

    @RunWith(Enclosed.class)
    public static class Rangeがマイナス5_1から5_1で {

        static Range sut = new Range(-5.1, 5.1);

        @RunWith(Theories.class)
        public static class 範囲外の場合 {

            @Ignore
            public static class Supplier extends ParameterSupplier {
                @Override
                public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
                    Fixture fixture1 = new Fixture(-5.2, false);
                    Fixture fixture2 = new Fixture(5.2, false);
                    return Arrays.asList(new PotentialAssignment[] {
                            PotentialAssignment.forValue(fixture1.toString(), fixture1),
                            PotentialAssignment.forValue(fixture2.toString(), fixture2)
                    });
                }
            }

            @Theory
            public void containsはfalseを返す(@ParametersSuppliedBy(Supplier.class)Fixture fixture) throws Exception {
                // Verify
                assertThat(sut.contains(fixture.value), is(fixture.expected)); 
            }
        }

        @RunWith(Theories.class)
        public static class 範囲内の場合 {

            @Ignore
            public static class Supplier extends ParameterSupplier {
                @Override
                public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
                    Fixture fixture1 = new Fixture(-5.1, true);
                    Fixture fixture2 = new Fixture(5.1, true);
                    return Arrays.asList(new PotentialAssignment[] {
                            PotentialAssignment.forValue(fixture1.toString(), fixture1),
                            PotentialAssignment.forValue(fixture2.toString(), fixture2)
                    });
                }
            }

            @Theory
            public void containsはtrueを返す(@ParametersSuppliedBy(Supplier.class)Fixture fixture) throws Exception {
                // Verify
                assertThat(sut.contains(fixture.value), is(fixture.expected)); 
            }
        }
    }

    @RunWith(Enclosed.class)
    public static class Rangeが0から10_5で {

        static Range sut = new Range(0.0, 10.5);

        @RunWith(Theories.class)
        public static class 範囲外の場合 {

            @Ignore
            public static class Supplier extends ParameterSupplier {
                @Override
                public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
                    Fixture fixture1 = new Fixture(-0.1, false);
                    Fixture fixture2 = new Fixture(10.6, false);
                    return Arrays.asList(new PotentialAssignment[] {
                            PotentialAssignment.forValue(fixture1.toString(), fixture1),
                            PotentialAssignment.forValue(fixture2.toString(), fixture2)
                    });
                }
            }

            @Theory
            public void containsはfalseを返す(@ParametersSuppliedBy(Supplier.class)Fixture fixture) throws Exception {
                // Verify
                assertThat(sut.contains(fixture.value), is(fixture.expected)); 
            }
        }

        @RunWith(Theories.class)
        public static class かつ範囲内の場合 {

            @Ignore
            public static class Supplier extends ParameterSupplier {
                @Override
                public List<PotentialAssignment> getValueSources(ParameterSignature sig) {
                    Fixture fixture1 = new Fixture(0.0, true);
                    Fixture fixture2 = new Fixture(10.5, true);
                    return Arrays.asList(new PotentialAssignment[] {
                            PotentialAssignment.forValue(fixture1.toString(), fixture1),
                            PotentialAssignment.forValue(fixture2.toString(), fixture2)
                    });
                }
            }

            @Theory
            public void containsはtrueを返す(@ParametersSuppliedBy(Supplier.class)Fixture fixture) throws Exception {
                // Verify
                assertThat(sut.contains(fixture.value), is(fixture.expected)); 
            }
        }
    }


    static class Fixture {
        double value; // テストデータ
        boolean expected; // 期待値

        Fixture(double value, boolean expected) {
            this.value = value;
            this.expected = expected;
        }

        @Override
        public String toString() {
            return String.format("when value=%s, expected=%s", value, expected);
        }
    }
}

5. まとめ

個人的にはパターン2で充分かなと思っています。
Fixture を使ってテストデータと期待値をまとめておけるのでそれだけでテストの可読性が上がると思います。

@ParametersSuppliedBy を使っても同じようなことができますけど、テストは複雑なことはしないで多少ベタな書き方のほうが自分にも周りにも優しいのかなといったところでしょうか。

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

技術評論社 2012-11-21
売り上げランキング : 24463

Amazonで詳しく見る by G-Tools

Googleアドセンス用(PC)

  • このエントリーをはてなブックマークに追加
  • follow us in feedly

関連記事

icatch-8536919558_0e01e5dc77-thumbnail

JUnit4 で JavaBeans の assertThat を簡潔に書きたい

今回も JUnit ネタです。 前回は List の assertThat を簡潔に書く方法につい

記事を読む

junit-logo

JUnit4 の Enclosed を使ったテストクラスサンプル

JUnit 本である「JUnit実践入門」を読んでいます。 体系的にまとめられている良書で、勉強にな

記事を読む

junit-logo

JUnit4 で List の assertThat を簡潔に書きたい

Java でプログラムを書くときは、プライベート・仕事を問わず JUnit を使ってユニットテストを

記事を読む

Googleアドセンス用(PC)

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


九 − = 4

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Googleアドセンス用(PC)

icatch-jersey_multi_pathparams
Jerseyの@PathParamはスラッシュの間に複数指定できる

http://hoge-api/user/{id}.{format}

icatch-vagrant_box_customize
VagrantのBoxファイルをカスタマイズして独自のBoxファイルを作成する

配布されている Vagrant の Box ファイルを使って検証環境を

icatch-2015-006-1
バリデーションチェックにJava8のOptionalを使ってスマートに書く(自分比)

Web アプリのバリデーションチェックにアノテーションを使うことが増え

icatch-2015-005-1
ユニットテストの偏りを防ぐ命名規則の付け方

ユニットテスト名に以下の命名規則を付けるようにして二ヶ月ぐらい経った。

icatch-2015-004-1
Vagrantで起動したCentOS上のOctopressをホストOSから確認する設定

タイトルの通りだが、Vagrant を使って起動した CentOS に

→もっと見る

PAGE TOP ↑