mike-neckのブログ

Java or Groovy or Swift or Golang

JUnit5入門(2) - アサーション

前回 の続き

今回は値の比較、アサーションについて。


JUnit5が提供するアサーション

JUnit5はJUnit4とほぼ同等のAssertion機能を提供している。また、より高度なAssertionを求める場合は、サードパーティライブラリーの利用を勧めている。

JUnit5のAssertionの基本的なもの

以下の通り。なお、assertThat はなくなった。また、メッセージはこれまでは引数の最初に渡していたが、引数の最後に渡すようになった。

メソッド 比較内容
Assertions#assertEquals 左(期待値)と右(実際の値)が等しいことを確認する
Assertions#assertNotEquals 左(期待値)と右(実際の値)が等しくないことを確認する
Assertions#assertTrue 値が true であることを確認する
Assertions#assertFalse 値が false であることを確認する
Assertions#assertSame 左(期待するオブジェクト)と右(実際のオブジェクト)が同じインスタンスであることを確認する
Assertions#assertNotSame 左(期待しないオブジェクト)と右(実際のオブジェクト)が同じインスタンスでないことを確認する
Assertions#assertNull 値が null であることを確認する
Assertions#assertNotNull 値が null でないことを確認する
Assertions#assertArrayEquals 左の配列(期待値)と右の配列(実際の値)が順番、値ともに等しい配列であることを確認する
サンプルコード
@Test
void assertVariation() {
  assertEquals("2nd test", getText(), "メッセージは3つめの引数");
  assertEquals("2nd test", getText(), () -> "メッセージは Supplier でも渡せる");
  assertTrue(true, "これは [true]");
  assertFalse(false); // メッセージなし
  assertNotEquals("いろいろなassertion", getText());
  assertNotSame(new Something(), new Something(), () -> "notSame は reference の比較");
  assertSame(something, something);
  assertNotSame(something, new Something(), () -> "型が違うけど比較できる...");
  assertNull(returnsNull());
  assertNotNull(getText());
  assertArrayEquals(new int[]{1,2,3,4,5,6,7,8}, intArray(), () -> "配列のテスト");
}

private static final Object something = new Object();
private static class Something {}
@NotNull
private String getText() {
    return "2nd test";
}
@Nullable
private String returnsNull() {
  return null;
}
@NotNull
private static int[] intArray() {
  return new int[]{1, 2, 3, 4, 5, 6, 7, 8};
}

追加になったAssertion

Iterable の比較をおこなうassertionメソッド(Assertions#assertIterableEquals)が追加された。

サンプルコード
@Test
void assertionForCollections() {
  assertIterableEquals(Arrays.asList(
      new Identity<>(0)
      , new Identity<>(1)
      , new Identity<>(2)
      , new Identity<>(3)
  ), identityList());
}

@Data @RequiredArgsConstructor
static class Identity<T> {
  private final T id;
}
@NotNull
private List<Identity> identityList() {
  return IntStream.range(0, 4)
      .mapToObj(Identity::new)
      .collect(toList());
}

便利なAssertion

Assertions#assertAll という複数のAssertをサポートするメソッドが追加された。

可変長引数で渡された Executable(Assertionの式)または Stream<Executable> を受取りすべてのAssertionをすべて実行し、どれがfailしたかエラー表示する。

サンプルコード
@Test
void assertAllThatFails() {
  Stream<Executable> tests = IntStream.rangeClosed(1, 7)
      .mapToObj(i -> () -> {
        log.info("This is assertion[{}]", i);
        assertEquals(i, i + (i % 3 == 0? 1 : 0), () -> "assertion" + i);
      });
  assertAll(tests);
}
実行結果
19:06:46.940 [INFO  com.example.ex2.FourthTest] - This is assertion[1]
19:06:46.945 [INFO  com.example.ex2.FourthTest] - This is assertion[2]
19:06:46.946 [INFO  com.example.ex2.FourthTest] - This is assertion[3]
19:06:46.947 [INFO  com.example.ex2.FourthTest] - This is assertion[4]
19:06:46.947 [INFO  com.example.ex2.FourthTest] - This is assertion[5]
19:06:46.948 [INFO  com.example.ex2.FourthTest] - This is assertion[6]
19:06:46.948 [INFO  com.example.ex2.FourthTest] - This is assertion[7]

Failures (1):
  JUnit Jupiter:FourthTest:assertAllThatFails()
    MethodSource [className = 'com.example.ex2.FourthTest', methodName = 'assertAllThatFails', methodParameterTypes = '']
    => org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures)
        assertion3 ==> expected: <3> but was: <4>
        assertion6 ==> expected: <6> but was: <7>

なお、複数のAssertionを実行してもテストは1としてしかカウントされない。

JUnit5のAssumption

Assumptionは条件に合致した場合のみテストを実行し、条件に合致しない場合にはテストをスキップするヘルパーメソッド。JUnit4にあった assumeThatassumeNotNullassumeNoException がなくなり、 assumeTrue が残った。また、 assumeFalseassumingThat が追加された。

サンプルコード
@Test
void thisTestDoNotRunOnSunday() {
  final DayOfWeek dayOfWeek = today().getDayOfWeek();
  assumeFalse(SUNDAY.equals(dayOfWeek));
  log.info("日曜以外");
  assertFalse("日".equals(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.JAPAN)));
}
@Test
void thisTestRunOnSunday() {
  final DayOfWeek dayOfWeek = today().getDayOfWeek();
  assumeTrue(SUNDAY.equals(dayOfWeek));
  log.info("日曜");
  assertEquals("日", dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.JAPAN));
}

private static final ZoneId ZONE_ID = ZoneId.of("Asia/Tokyo");
@NotNull @Contract(pure = true)
private static LocalDate today() {
  return LocalDate.now(ZONE_ID);
}
実行結果
Test run finished after 10468 ms
[         2 containers found      ]
[         0 containers skipped    ]
[         2 containers started    ]
[         0 containers aborted    ]
[         2 containers successful ]
[         0 containers failed     ]
[         1 tests found           ]
[         0 tests skipped         ]
[         1 tests started         ]
[         1 tests aborted         ]
[         0 tests successful      ]
[         0 tests failed          ]

assumingThat は最初の引数の BooleanSuppliertrue を返した場合のみ後続の Executable(Assertion)が実行される。

assumingThat のサンプルコード
@Test
void thisTestDoNotRunOnSunday() {
  final DayOfWeek dayOfWeek = today().getDayOfWeek();
  assumingThat(
      () -> !SUNDAY.equals(dayOfWeek)
      , () -> assertFalse("日".equals(dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.JAPAN)))
  );
}
@Test
void thisTestRunOnSunday() {
  final DayOfWeek dayOfWeek = today().getDayOfWeek();
  assumingThat(
      () -> SUNDAY.equals(dayOfWeek)
      , () -> assertEquals("日", dayOfWeek.getDisplayName(TextStyle.SHORT, Locale.JAPAN))
  );
}
実行結果
Test run finished after 10261 ms
[         2 containers found      ]
[         0 containers skipped    ]
[         2 containers started    ]
[         0 containers aborted    ]
[         2 containers successful ]
[         0 containers failed     ]
[         2 tests found           ]
[         0 tests skipped         ]
[         2 tests started         ]
[         0 tests aborted         ]
[         2 tests successful      ]
[         0 tests failed          ]

コードはこちらから

github.com

How to run test class whose name doesn't end with Test in JUnit5 with Gradle.

JUnit5 publishes junit-platform-gradle-plugin. With it you can run JUnit5 tests with gradle. But if you don't give Test at the end of the test class name, this plugin ignores to run it. If you want to run a test classes whose name don't end with Test, ex. FooSpec, BarTestCase, you have to specify a name pattern of them via includeClassName(or includeClassNames) in junitPlatform.filters Closure.

example
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.junit.platform:junit-platform-gradle-plugin:1.0.0-M3"
  }
}

apply plugin: 'java'
apply plugin: 'org.junit.platform.gradle.plugin'

repositories {
  mavenCentral()
  jcenter()
}

dependencies {
  testCompile "org.junit.jupiter:junit-jupiter-api:5.0.0-M3"
  testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.0-M3"
}

// specify your test class names here
junitPlatform {
  filters {
    includeClassNamePattern '^.*TestCase$'
    includeClassNamePatterns '^.*Spec$', '^.*Tests?$' 
  }
}

With this configuration, you can run tests whose name ends with Test, Tests, Spec, TestCase.

JUnit5入門(1) - テストクラスの作成とテストの実行

年末にかけてJUnit5(junit-jupiter)をいじったのでまとめ。

使い方的な話はQiitaにある記事のほうが詳しいかもしれない…

qiita.com

qiita.com

qiita.com

qiita.com


JUnit5ライブラリーの導入

テストコンパイルスコープにjunit-jupiter-api、テストランタイムにjunit-jupiter-engineを用いるようにする。

build.gradle
repositories {
  mavenCentral()
  jcenter()
}

dependencies {
  testCompile "org.junit.jupiter:junit-jupiter-api:5.0.0-M3"
  testRuntime "org.junit.jupiter:junit-jupiter-engine:5.0.0-M3"
}
pom.xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-api</artifactId>
  <version>5.0.0-M3</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter-engine</artifactId>
  <version>5.0.0-M3</version>
  <scope>test</scope>
</dependency>

JUnit5のテストクラスとテストメソッドの作り方

JUnit4とほとんど変わらない。プレーンなクラスを作って、 @Test アノテーションをテストメソッドに付加するだけで実行してくれる。

サンプルコード
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

@Slf4j
public class SimpleTest {

    @Test
    void firstTest() {
        log.info("1st test");
    }
}

@Test アノテーションを付与するメソッドは、staticメソッドではなく、またprivateなメソッドでない必要がある。

実行結果
1 01, 2017 12:55:22 午後 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
情報: Discovered TestEngines with IDs: [junit-jupiter]
12:55:28.796 [INFO  com.example.ex1.SimpleTest] - 1st test

コードサンプル


JUnit5の実行方法

IDEからの実行

IntelliJ IDEA

デフォルトの状態でJUnit5を実行できるようになっている。なお、実行方法はこれまでと変わらない。

f:id:mike_neck:20170101154113p:plain

テストクラスの左側にある実行ボタンをクリックして、メニューから「Run クラス名 」を実行する。

f:id:mike_neck:20170101154237p:plain

f:id:mike_neck:20170101154313p:plain

Eclipse

知らない…ごめん…

Bug 488566 - [JUnit][JUnit 5] Add support for JUnit 5を読むかぎり、Eclipse 4.7 M4にてJunit5のサポートがあるようです。

Netbeans

知らないし、ググっても出てこないけど、console-launcherは単なるjavaアプリケーションなので必要なライブラリーをclasspathに指定しつつ、console-launcherを起動すれば実行できる。

Gradleからの実行

Gradleにjunit-platformプラグインを導入して、 junitPlatformTest タスクを実行するとテストを実行できる。

build.gradle
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.junit.platform:junit-platform-gradle-plugin:1.0.0-M3"
  }
}

apply plugin: 'java'
apply plugin: 'org.junit.platform.gradle.plugin'
実行結果

junitPlatformTest は長いので jPT で指定することもできる。

$ gradle jPT
:compileJava
:processResources
:classes
:compileTestJava
:processTestResources
:testClasses
:junitPlatformTest
12 31, 2016 9:39:27 午後 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
情報: Discovered TestEngines with IDs: [junit-jupiter]
21:39:33.471 [INFO  com.example.ex1.SimpleTest] - 1st test
Test run finished after 11642 ms
[          1 containers found      ]
[         0 containers skipped    ]
[         1 containers started    ]
[         0 containers aborted    ]
[         1 containers successful ]
[         0 containers failed     ]
[         1 tests found           ]
[         0 tests skipped         ]
[         1 tests started         ]
[         0 tests aborted         ]
[         1 tests successful      ]
[         0 tests failed          ]

BUILD SUCCESSFUL

Total time: 12.576 secs

注意点

GradleでJunit5を起動するときに注意したい所は次の点

  • Gradleのバージョンは2.5以上が必要
  • junit-platformjunit-jupiter とがややこしい
    • コンパイル時に必要なライブラリーのgroupが org.junit.jupiter で、gradleのプラグインのほうのgroupが org.junit.platform
    • junit-jupiter のバージョン(いわゆるJUnit5のバージョン)が 5.0.0.-M3junit-platform のバージョン(つまりgradleプラグインのバージョン)が 1.0.0-M3
  • 独自のJUnit5を走らせるタスクを作るのが非常に面倒くさい
    • junitPlatformTest タスクは単なる JavaExec タスクなので、同じようなものを作ろうとするとこれと同じようなコードを書かないといけない
      • まあ、そうではあるんだけど、gradleのissueにJunit5関連のissueが立っているので、gradleから公式のタスクが出るかもしれない
  • デフォルトでクラス名が Test あるいは Tests で終わるものだけをテストクラスとして読み取るようになっている(これとかこれなど)
    • そのため FooSpec というクラスに @Test を付与してもgradleからは起動できない
    • これを回避するためには次のように junitPlatform.filters.includeClassNamePattern あるいは junitPlatform.filters.includeClassNamePatterns に実行したいクラス名のパターンを指定する必要がある
junitPlatform {
  filters {
    includeClassNamePattern '^.*Spec$'
    // あるいは
    includeClassNamePatterns '^.*Spec$', '^.*Tests?$' 
  }
}

DateTimeFormatterのパターンとLocale

Junit5の ParameterResolver によってテストメソッドのパラメーターに渡せる日付文字列の形式をユーザーが自由に決められるようにするために、 DateTimeFormatter を使っていた際に、どうしても月のパターン MMM によって月が名前(例えば1月なら Jan)にならないので、小3時間くらい悩んでました。

結果的には DateTimeFormatter によってフォーマットされる日付の文字列は DateTimeFormatter に渡される Locale も関係するという初歩的だけど、ググってもあまり出てこない(くらいに初歩的な)事実を知らなかったがためにドハマリする結果になりました。

そこで、 LocaleDateTimeFormatter のパターンの組み合わせでどのような日付が出力できるか確認してみました(とはいっても)。

表示内容 パターン ja_JP en_US zh_CN
Y 2017 2017 2017
YY 17 17 17
YYYY 2017 2017 2017
u 2017 2017 2017
uu 17 17 17
uuuu 2017 2017 2017
四半期 q 1 1 1
四半期 qq 01 01 01
四半期 qqq 1 1 1
四半期 Q 1 1 1
四半期 QQ 01 01 01
四半期 QQQ Q1 Q1 1季
四半期 QQQQ 第1四半期 1st quarter 第1季度
M 1 1 1
MM 01 01 01
MMM 1 Jan 一月
MMMM 1月 January 一月
L 1 1 1
LL 01 01 01
LLL 1 1 一月
d 1 1 1
dd 01 01 01
曜日 e 1 1 1
曜日 ee 01 01 01
曜日 eee Sun 星期日
曜日 eeee 日曜日 Sunday 星期日
曜日 c 1 1 1
曜日 ccc Sun 星期日
曜日 cccc 日曜日 Sunday 星期日
曜日 E Sun 星期日
曜日 EE Sun 星期日
曜日 EEE Sun 星期日
曜日 EEEE 日曜日 Sunday 星期日

おわり