LoginSignup
5
6

More than 3 years have passed since last update.

【Java】JUnit4のテストケース例

Last updated at Posted at 2018-12-21

@Ruleに慣れない

JUnit書いてますか?書いてますよね。私も書いてます。
私は官公庁の案件が多めなんですけど、官公庁とか金融とかお堅いところの案件って大抵環境が古いんですよね!
未だにJava6とかあるあるです。……2019年現在完全新規の案件でもJava1.6+Spring1.xとかで作らされる現場とか普通にあるので…これが官公庁と有名企業(IT企業以外)の案件のリアルな気がしています:joy:
最初に触ったときはまだJUnit3だったんですけど、今は流石にJUnit4が主流です。
わざわざ既存プロジェクトでテスティングフレームを乗せ替えることがないのもあり、業務(お堅い系企業の案件)ではJUnit5を見たことはありません。

でも、未だJUnit4に慣れないんですよね……
というわけで、毎回ググったり人のコード探して書いてる@Ruleアノテーション関連で、よく書くパターンを覚書き。お品書きとしては、こんな感じ。

  • アサートエラー発生時に止めないでため込む(ErrorCollector)
  • テストメソッド名を取得(TestName)
  • 発生した例外のアサートを行う(ExpectedException)
  • テスト前後や成功・失敗時の処理を規定(TestWatcher)

PowerMock使用時について

補足2でちょこっとだけ触れていますが、PowerMockと@Ruleの相性はとってもよろしくないです。
PowerMockのPowerMockRuleは使えない、classに@RunWithアノテーション(@RunWith(PowerMockRunner.class))という認識。
そんでな、これでnotコンパイルエラーyes実行エラー(PowerMock関連、もしくは@Rule付けたオブジェクトがnullになるとかうまく動かない)が出たら、@RuleかPowerMockのどっちかを諦めるしかないやつ。
バージョンの組み合わせによっては解決するのかもしれないんですが、業務はバージョン固定なので試行錯誤無理なので正解に至ってないですすいません。
あとコンストラクタをPowerMockでモックすると、EclEmmaではカバレッジが取得できない問題とかもあります。(これはバージョンの組み合わせによっては解決する)

サンプルコード

テスト対象

Sample.java
public class Sample {
    public static boolean isEmpty(String value) {
        return (value == null || "".equals(value)) ? true : false;
    }

    public static int parseInt(String value) throws Exception {
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new Exception("変換失敗", e);
        }
    }
}

テストコード

モックの設定とかアサートが複雑な場合はケース毎にメソッド切りますけど、汎用的でstaticなユーティリティメソッドのテストケースは、こうやって複数ケースを1テストメソッドで実施しちゃいます。
引数がマップの時なんかは、/区切りのStringを引数としたprivateメソッド切ってマップに変換してから渡したりします。テストクラスでもよくprivateメソッド切る派。

SampleTest.java
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;

import java.text.DecimalFormat;
import java.text.MessageFormat;

import org.hamcrest.core.IsInstanceOf;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ErrorCollector;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestName;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.hamcrest.Matcher;

public class SampleTest {
    // アサートエラーの溜め込み
    @Rule
    public ErrorCollector errs = new ErrorCollector();
    // テストメソッド名の取得
    @Rule
    public TestName name = new TestName();
    // 例外アサート内容設定
    @Rule
    public ExpectedException thrown = ExpectedException.none();
    // 開始・終了メッセージの出力
    @Rule
    public TestWatcher watcher = new TestWatcher() {
        @Override
        protected void starting(final Description desc) {
            System.out.println(MessageFormat.format("Test start. : {0}", desc));
        }

        @Override
        protected void succeeded(final Description desc) {
            System.out.println(MessageFormat.format("Test succeeded. : {0}", desc));
        }

        @Override
        protected void failed(final Throwable e, final Description desc) {
            System.err.println(MessageFormat.format(
                "Test failed. : {0} \r\n*** Exception : {1}.", desc, e.getMessage()));
        }

        @Override
        protected void finished(final Description desc) {
            System.out.println(MessageFormat.format("Test Finished. : {0}", desc));
        }
    };

    @Test
    public void test_isEmpty() {
        String[][] args = new String[][]{
            new String[]{null, "true"},
            new String[]{"", "true"},
            new String[]{"ABC", "false"},
        };

        int i = 1;
        for (String[] arg : args) {
            // アサートエラー
            this.errs.checkThat(this.getAssertMsg(i++), 
                    Sample.isEmpty(arg[0]), is(Boolean.valueOf(arg[1])));
        }
    }

    @Test
    public void test_parseInt_異常系() throws Exception {
        // スローされた例外のアサート
        this.thrown.expect(Exception.class);
        this.thrown.expectMessage("変換失敗");
        this.thrown.expectCause(IsInstanceOf.<Throwable>instanceOf(NumberFormatException.class));

        Sample.parseInt("AAA");
    }

    private String getAssertMsg(int idx) {
        // テストメソッド名取得
        return this.name.getMethodName() + " : " + new DecimalFormat("000").format(idx);
    }
}

ErrorCollector

昔ながらのassert○○でアサートエラーが発生すると、テストメソッドはそこで終了します。
上記ソースのtest_isEmpty()のようにテスト実行とアサートを繰り返すようなテストメソッドの場合、1件目でアサートエラーが発生すると2件目以降の結果は、1件目のアサートエラーをどうにかしないと確認できません。
ErrorCollectorを使用することで、途中で発生したアサートエラーをストックして次の処理をしてくれます。
ストックされたアサートエラーは、EclipseのJUnitビューからテストメソッドを選択して障害トレース欄で確認できます。

TestName

ErrorCollectorと組み合わせて使うのがオススメ。
アサートエラーがどこで起きたか把握しやすくするために、checkThatの第一引数にメッセージを渡したりとか。
上記ソースだとgetAssertMsg(int idx)の中で使ってます。
TestNameを使わないテストメソッド名の取得例は、補足1にて。

ExpectedException

例外アサートは@Test(expected = Exception.class)って書き方もあるのですが、これだとスローされた例外の型のみの検証になります。メッセージとか原因例外は検証できない…
なので以前はtry~catchで括って書いてたんですけど、@Rule使えるなら使ったほうがサクッと書けます。
上記ソースでは原因例外の検証は型だけしかやってませんが、下記のような感じで同時にメッセージとかも検証できます。

thrown.expectCause(allOf(
    instanceOf(NumberFormatException.class),
    hasProperty("message", is("メッセージ内容"))));

但しExpectedException.expectCauseは古いバージョンだとコンパイル通らなくて悪戦苦闘したりりするので、そういう時はさっくり諦めてtry~catchで書くほうがいいです。
ライブラリを更新しないままの既存プロジェクトとかで引っかかるやーつ…JUnit4.12以降ならば例の書き方でOKだと思います。4.10だとダメだったよ…

TestWatcher

Junit4.11以降で使えるやつ。テスト実行前、前提条件不成立時、テスト成功時、テスト失敗時、テスト終了後に呼び出される処理を書けます。
前提条件不成立時は上記ソースには書いてないですが、skipped(AssumptionViolatedException, Description)ですね。使ったことないですけど。
上記ソースではログをコンソールに出力してますが、もちろんロガーを使えばコンソールだけではなく好きなように出力できます。
そもそもログ出力のためと決まっているわけではないので、@Before@Afterではなくこっちを使ってもいいかと。

@BeforeClass@Before@After@AfterClassと組み合わせた実行順序は、検証結果を掲載している記事があったので下記をどうぞ。
JUnit4 の実行順序 - umezucolorの日記
この実行順序なら、個人的にはTestWatcherで規定する処理はログ出力くらいかな…と思います。
例えばテストメソッド実行毎にファイル削除が必要、なんて場合は@Beforeで書くかなあ…


実行例

EclipseだとJUnitビューの障害トレースのところに、アサートエラーになったケースがまとめて出力されます。
アサートエラーになるような想定結果で、コンソールから実施した結果はこんな感じ。
ちなみにコンソールから実行するときは、こんな感じで実行します。libにはJUnitのjarを入れてます。
java -cp ./;./lib/*; org.junit.runner.JUnitCore SampleTest

JUnit version 4.12
.Test start. : test_parseInt_異常系(SampleTest)
Test succeeded. : test_parseInt_異常系(SampleTest)
Test Finished. : test_parseInt_異常系(SampleTest)
.Test start. : test_isEmpty(SampleTest)
Test failed. : test_isEmpty(SampleTest)
*** Exception : There were 2 errors:
  java.lang.AssertionError(test_isEmpty : 002
Expected: is <true>
     but: was <false>)
  java.lang.AssertionError(test_isEmpty : 003
Expected: is <false>
     but: was <true>).
Test Finished. : test_isEmpty(SampleTest)
EE
Time: 0.043
There were 2 failures:
1) test_isEmpty(SampleTest)
java.lang.AssertionError: test_isEmpty : 002
Expected: is <true>
     but: was <false>
        at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
        at org.junit.Assert.assertThat(Assert.java:956)
        at org.junit.rules.ErrorCollector$1.call(ErrorCollector.java:65)
        at org.junit.rules.ErrorCollector.checkSucceeds(ErrorCollector.java:78)
        at org.junit.rules.ErrorCollector.checkThat(ErrorCollector.java:63)
        at SampleTest.test_isEmpty(SampleTest.java:64)
        ~中略~
2) test_isEmpty(SampleTest)
java.lang.AssertionError: test_isEmpty : 003
Expected: is <false>
     but: was <true>
        at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:20)
        at org.junit.Assert.assertThat(Assert.java:956)
        at org.junit.rules.ErrorCollector$1.call(ErrorCollector.java:65)
        at org.junit.rules.ErrorCollector.checkSucceeds(ErrorCollector.java:78)
        at org.junit.rules.ErrorCollector.checkThat(ErrorCollector.java:63)
        at SampleTest.test_isEmpty(SampleTest.java:64)
        ~中略~

FAILURES!!!
Tests run: 2,  Failures: 2

補足1

テストメソッド名はスタックトレースからでも取得可能です。遅いけど。昔から知られた方法ですね。
getStackTrace[2]のところがスタックトレースの深さなので、JUnit内でテストメソッドを呼び出す階層が一定じゃない場合場合は調整が必要かも。

private String createAssertMsg(int idx) {
    return Thread.currentThread().getStackTrace[2].getMethodName() + " : " + new DecimalFormat("000").format(idx);
}

補足2

なお@Rule周りはPowerMock使おうと思って@RunWith(PowerMockRunner.class)書くと使えない……時があります。
完全にダメかというとそうでもなくて、TestNameやErrorCollectorは利いたり利かなかったりするんだけど何でだろう…
ちなみに上記2つが使えたのはSpringのDIも組み合わせたくてクラスのアノテーションがこんな感じだった時。
まあでも例外検証のExpectedExceptionはこのケースでも利かなかったので、try~catchで書きました…

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@PowerMockIgnore("javax.crypto.*")
public class SampleTest {

@PowerMockIgnore("javax.crypto.*")はPowermockとSpringの組み合わせでjavax.crypto.Cipherを使うと、普通は起きないClassCastExceptionが発生するのでその対応です。
備忘録代わりなのでついでにメモ。

PowerMock使うと特定のテストメソッドだけの実行ができなくなる場合もあるので、そうなったら確認したいテストメソッド以外には@Testではなく@Ignoreを付けたりします。
PowerMockはできることを増やすけど、できないことも増やすのでちょっとナー…と思うときもある。
……んですけど、staticメソッドをモックできるのは大きいんですよね…テスト対象の中で呼び出されてる標準APIでエラー起こしたいときとかさ…

そのうちRuleChainについても書きたいです。

5
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6