JUnit 5 + Gradle による Java の自動テスト導入

## 概要
- JUnit 5 + Gradle による Java の自動テストについて基本的なサンプルを書く

## Junit 5 とは

- JUnit は Java の自動テスト用フレームワーク
- JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
- JUnit Platform: テスト実行プラットフォーム
- JUnit Jupiter: テストコード実装用APIとテストエンジンを提供
- JUnit Vintage: JUnit 3 と JUnit 4用のテストコード実装用APIとテストエンジンを提供

## 今回の環境
- Java 11 (OpenJDK 11.0.2)
- JUnit Platform 1.5.2
- JUnit Jupiter 5.5.2
- Gradle 6.0.1

## JUnit 5 + Gradle の基本的なサンプル

### ソースコード一覧

```
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   └── java
    │       └── myapp
    │           └── Calc.java
    └── test
        └── java
            └── myapp
                └── CalcTest.java
```

### build.gradle

```build.gradle 
plugins {
  id 'java'
}

repositories {
  jcenter()
}

dependencies {
  // Junit Jupiter 5.5.2 を導入
  // 依存関係で以下等が導入される
  // junit-jupiter-api:5.5.2
  // junit-jupiter-engine:5.5.2
  // junit-jupiter-platform-engine:1.5.2
  testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'
}

test {
  // JUnit platform を使う設定
  useJUnitPlatform()

  testLogging {
    // テスト時の標準出力と標準エラー出力を表示する
    showStandardStreams true
    // イベントを出力する (TestLogEvent)
    events 'started', 'skipped', 'passed', 'failed'
    // 例外発生時の出力設定 (TestExceptionFormat)
    exceptionFormat 'full'
  }
}
```

参考:

- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
- [Maven Repository: org\.junit\.jupiter » junit\-jupiter](https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter)
- [TestLogging \- Gradle DSL Version 6\.0\.1](https://docs.gradle.org/6.0.1/dsl/org.gradle.api.tasks.testing.logging.TestLogging.html)
- [TestLogEvent \(Gradle API 6\.0\.1\)](https://docs.gradle.org/6.0.1/javadoc/org/gradle/api/tasks/testing/logging/TestLogEvent.html)
- [TestExceptionFormat \(Gradle API 6\.0\.1\)](https://docs.gradle.org/6.0.1/javadoc/org/gradle/api/tasks/testing/logging/TestExceptionFormat.html)

### settings.gradle

```settings.gradle
rootProject.name = 'myapp'
```

### Calc.java

```java
package myapp;

public class Calc {

  private int base;

  // 基準となる値を設定
  public Calc(int base) {
    this.base = base;
  }

  // 足す
  public int plus(int num) {
    return base + num;
  }

  // 引く
  public int minus(int num) {
    return base - num;
  }
}
```

### CalcTest.java

```java
package myapp;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class CalcTest {

  // テスト開始前に1回だけ実行される
  @BeforeAll
  static void beforeAll() {
    System.out.println("CalcTest 開始");
  }

  // テスト開始後に1回だけ実行される
  @AfterAll
  static void afterAll() {
    System.out.println("CalcTest 終了");
  }

  // 各テストメソッド開始前に実行される
  @BeforeEach
  void beforeEach() {
    System.out.println("CalcTest のテストメソッドをひとつ開始");
  }

  // 各テストメソッド開始後に実行される
  @AfterEach
  void afterEach() {
    System.out.println("CalcTest のテストメソッドをひとつ終了");
  }

  // テストメソッドは private や static メソッドにしてはいけない
  // 値を返してもいけないので戻り値は void にする
  @Test
  void testPlus() {
    System.out.println("testPlus を実行: 2 + 3 = 5");
    Calc calc = new Calc(2);
    // 第1引数: expected 想定される結果
    // 第2引数: actual 実行結果
    // 第3引数: message 失敗時に出力するメッセージ
    assertEquals(5, calc.plus(3), "2 + 3 = 5 の検証");
  }

  @Test
  void testMinus() {
    System.out.println("testMinus を実行: 5 - 2 = 3");
    Calc calc = new Calc(5);
    assertEquals(3, calc.minus(2), "5 - 2 = 3 の検証");
  }
}
```

参考:

- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
- [Assertions \(JUnit 5\.5\.2 API\)](https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Assertions.html)
- [Test \(JUnit 5\.5\.2 API\)](https://junit.org/junit5/docs/current/api/org/junit/jupiter/api/Test.html)

### テスト成功時の例

```
$ gradle test

> Task :test

myapp.CalcTest STANDARD_OUT
    CalcTest 開始

myapp.CalcTest > testMinus() STARTED

myapp.CalcTest > testMinus() STANDARD_OUT
    CalcTest のテストメソッドをひとつ開始
    testMinus を実行: 5 - 2 = 3
    CalcTest のテストメソッドをひとつ終了

myapp.CalcTest > testMinus() PASSED

myapp.CalcTest > testPlus() STARTED

myapp.CalcTest > testPlus() STANDARD_OUT
    CalcTest のテストメソッドをひとつ開始
    testPlus を実行: 2 + 3 = 5
    CalcTest のテストメソッドをひとつ終了

myapp.CalcTest > testPlus() PASSED

myapp.CalcTest STANDARD_OUT
    CalcTest 終了

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed
```

### テスト失敗時の例

```
$ gradle test

> Task :test FAILED

myapp.CalcTest STANDARD_OUT
    CalcTest 開始

myapp.CalcTest > testMinus() STARTED

myapp.CalcTest > testMinus() STANDARD_OUT
    CalcTest のテストメソッドをひとつ開始
    testMinus を実行: 5 - 2 = 3
    CalcTest のテストメソッドをひとつ終了

myapp.CalcTest > testMinus() FAILED
    org.opentest4j.AssertionFailedError: 5 - 2 = 3 の検証 ==> expected: <3> but was: <7>
        at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
        at org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)
        at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
        at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:542)
        at myapp.CalcTest.testMinus(CalcTest.java:48)
```

## assertAll でまとめてテストするサンプル

### サンプルコード

```java
package myapp;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

class CalcTest {

  @Test
  void testPlus() {
    Calc calc = new Calc(10);
    // まとめて検証
    // 途中で失敗しても停止せずにすべて検証する
    assertAll(
     () -> assertEquals(30, calc.plus(20)),
     () -> assertEquals(99, calc.plus(90)),
     () -> assertEquals(11, calc.plus(50)),
     () -> assertEquals(40, calc.plus(30))
    );
  }
}
```

参考:

- [JUnit 5 User Guide - 2.4. Assertions](https://junit.org/junit5/docs/current/user-guide/#writing-tests-assertions)

### テスト失敗時の例

```
$ gradle test

> Task :test FAILED

myapp.CalcTest > testPlus() STARTED

myapp.CalcTest > testPlus() FAILED
    org.opentest4j.MultipleFailuresError: Multiple Failures (2 failures)
        org.opentest4j.AssertionFailedError: expected: <99> but was: <100>
        org.opentest4j.AssertionFailedError: expected: <11> but was: <60>
        at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:80)
        at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:44)
        at org.junit.jupiter.api.AssertAll.assertAll(AssertAll.java:38)
        at org.junit.jupiter.api.Assertions.assertAll(Assertions.java:2839)
        at myapp.CalcTest.testPlus(CalcTest.java:15)

1 test completed, 1 failed

FAILURE: Build failed with an exception.
```

## 例外発生テストのサンプル

```java
package myapp;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

class CalcTest {

  @Test
  void testPlus() {
    Calc calc = new Calc(100);
    // 0 で割ったら ArithmeticException が発生することを想定
    ArithmeticException e =
      assertThrows(ArithmeticException.class,
        () -> calc.divide(0));
    assertTrue(e instanceof ArithmeticException);
  }
}
```

## 参考資料

- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/)
- [Overview \(JUnit 5\.5\.2 API\)](https://junit.org/junit5/docs/current/api/)
- [GitHub \- junit\-team/junit5\-samples: Collection of sample applications using JUnit 5\.](https://github.com/junit-team/junit5-samples)
- [まだまだ間に合うJUnit\(再\)入門 \#jjug\_ccc \#ccc\_g4 \- Speaker Deck](https://speakerdeck.com/yusuke/madamadajian-nihe-ujunit-zai-ru-men-number-jjug-ccc-number-ccc-g4)
- [まだまだ間に合う！JUnit 5入門 / JJUG CCC 2018 Spring \- Speaker Deck](https://speakerdeck.com/rshindo/jjug-ccc-2018-spring)