Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

JUnit 4で消耗しているあなたに贈るJUnit 5入門

こんにちは、しんどーです。 気づいたら入社8ヶ月くらい経ってました。

さて、待望のJUnit 5のGA版が今年9月にリリースされました! この記事ではJUnit 5の概要と新機能の一部をご紹介したいと思います。 全部User Guideに書いてあるとか言わない

JUnit 5とは

JUnitとは、言わずと知れたJavaのテスティングフレームワークであり、 デファクトスタンダードの地位にあります。ですが現行のJUnit 4系の最初の メジャーバージョンリリースはすでに10年ほど前であり、保守性の低下が問題に なっていました。

そこでJUnitの刷新を目指すべく、JUnit 5プロジェクトが立ち上げられました。 Junit 5の特徴を簡単に述べると、

といったことが挙げられます。

JUnit 5のAPIは、JUnit 4のAPIとは互換性がありません。 したがって全く新しいフレームワークとして理解したほうがいいかもしれません。

で、JUnit 5って使えるの?

結論から言うと、一から再設計を行ったことによって、 JUnit 4のイマイチな部分が軒並み改善された印象です。

これまで設計上の制約からか、謎のお決まりごと(@Enclosedなクラスはstaticでないといけない等)が 多かったですが、JUnit 5はより自然で直感的なインターフェースデザインになっています。 お決まりコードも減りますので、多少のテストコード量の削減も期待できます。

ただし、すでに述べたようにJUnit 4とは互換性がありませんので、 既存のテストを書き換えるのはおすすめできません。 また新規のプロジェクトで使う場合も、IDE・ビルドツール・フレームワーク等の サポートにはご注意ください。 例えば、後述しますがJUnit 5.0系とmaven-surefire-pluginの2.20系の連携はバグがあり動作しません。

Springユーザであれば、Spring 5からJUnit 5のサポートがあるので、一緒に使い始めるのがいいかもしれません。 (逆にSpring 4でJUnit 5を使うのは大変かと思います。。)

JUnit 5のモジュール構成

JUnit 5は3つのサブプロジェクトから構成されています。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform

JUnitテストを実行するための基盤を提供します。Maven用、Gradle用のプラグイン等が用意されていますので、 各プロジェクトの環境に合わせたモジュールを選択します。

JUnit Jupiter

テストを記述するAPI(@Test etc...)とテストエンジンを提供します。 基本的に、「JUnit 5」と言った場合はJUnit JupiterのAPIを指すことが多いと思います。

JUnit Vintage

JUnit Platform上でJUnit 3 or 4を動かすためのテストエンジン等を提供します。 この記事では特に触れません。

Getting Started

さて、早速JUnit 5を動かしてみましょう。JUnit 4はたった一つのjarにまとまっていましたが、JUnit 5は複数の モジュールの組み合わせで動作します。(おかげで最初の1時間を無駄にしました。。)

シンプルなMavenプロジェクトを作成して、JUnit 5を動かします。

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.github.rshindo</groupId>
  <artifactId>junit5-sample</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <junit.jupiter.version>5.0.2</junit.jupiter.version>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <version>${junit.jupiter.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <version>${junit.jupiter.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.platform</groupId>
      <artifactId>junit-platform-launcher</artifactId>
      <version>1.0.2</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.7.0</version>
          <configuration>
            <source>${java.version}</source>
            <target>${java.version}</target>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.19.1</version>
          <dependencies>
            <dependency>
              <groupId>org.junit.platform</groupId>
              <artifactId>junit-platform-surefire-provider</artifactId>
              <version>1.0.2</version>
            </dependency>
          </dependencies>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

実はJUnit 5.0系 は現在 maven-surefire-plugin の最新版(2.20.x)に対応していません。(5.1でバグフィックスが入るようです)
https://github.com/junit-team/junit5/issues/809
ここでは 2.19.1 を利用しています。

pom.xmlを作成したら、次にテストクラスです。

import org.junit.jupiter.api.Test;
public class AppTest {

  @Test
  void testApp() {
    assertTrue(true);
  }
}

テストクラスを書いたら、最後にmvn testでテストを実行しましょう!

基本のテスト

JUnit 5のAPIJUnit 4とは互換性がありませんが、 基本的なアノテーションは パッケージと名前を変えるだけでほぼ同じように動きます。

JUnit4 JUnit5
@Test @Test
@Before @BeforeEach
@BeforeClass @BeforeAll
@After @AfterEach
@AfterClass @AfterAll
@Ignore @Disabled

前処理・後処理

前処理・後処理は @BeforeEach @BeforeAll @AfterEach @AfterAll を使います。

@BeforeAll
static void setUpAll() {
    System.out.println("before all tests");
}

@BeforeEach
void setUp() {
    System.out.println("before each test");
}

@Test
void test1() {
    System.out.println("test 1");
}

@Test
void test2() {
    System.out.println("test 2");
}

@AfterEach
void tearDown() {
    System.out.println("after each test");
}

@AfterAll
static void tearDownAll() {
    System.out.println("after all tests");
}

このときの出力は次のようになります。

before all tests
before each test
test 1
after each test
before each test
test 2
after each test
after all tests

テストケース名

テストの名前は @DisplayName で指定可能です。これまではテストメソッド名を日本語で書く人も多かったと思いますが、 @DisplayName であれば文字種の縛りもなく自由にテスト名を変えられます。

@Test
@DisplayName("1 + 1 = 2になるテスト")
void plusTest() {
    assertEquals(1 + 1, 2);
}

@Test
@DisplayName("😁") //絵文字も可能
void emojiTest() {
}

Eclipseで実行すると以下のように表示されます。

f:id:acro-engineer:20171207003848p:plain

注)mavenでレポート出力したら@DisplayNameが効いていませんでした。。

アサーション

ラムダ式に対応した、便利なアサートAPIが追加されています。 もちろん、assertTrueassertEquals もあります。

アサーションのグループ化

あるオブジェクトのプロパティを全て検証したいとき、これまでは以下のように書いていたと思います。

@Test
public void employeeTest() {
    Employee employee = employeeService.findById(1);

    assertEquals("Aoi", employee.getFirstName());
    assertEquals("Miyamori", employee.getLastName());
}

仮に1つ目のアサーションが失敗したとします。 その時点で例外が投げられ、その次のアサーションは実行されません。 1つ目のアサーションが通るように修正したら、 今度は2つ目のアサーションも失敗していることに気づいてまた修正に戻って・・・という経験をした方も多いと思います。

そんなときに役に立つのが、アサーションのグループ化です。

@Test
void employeeTest() {
    Employee employee = employeeService.findById(1);

    assertAll("employee",
        () -> assertEquals("Aoi", employee.getFirstName()),
        () -> assertEquals("Miyamori", employee.getLastName())
    );
}

assertAll の中にラムダ式で複数のアサーションを渡しています。 このように記述することで、片方のアサーションが失敗しても、 もう片方のアサーションを実行することができるのです!

例外のアサーション

JUnit 4での例外のアサーションはこうでした。

@Test(exception = NumberFormatException.class)
public void exceptionTest() {
    Long.valueOf(null); // throws NumberFormatException
    fail("Exception not thrown");
}

これが assertThrows によってこう変わります。

@Test
void succeedingTest() {
    NumberFormatException ex =
        assertThrows(NumberFormatException.class,
            () -> Long.valueOf(null));
    assertEquals(ex.getMessage(), "null");
}

assertThrows は、例外がthrowされなかった場合には アサーションが失敗となります。@Testには何も書きません。 assertThrows は例外オブジェクトを返しますので、 そこからメッセージの検証などもできます。

また、JUnit 4で例外が発生しなかったケースのために書いていた fail("Exception not thrown") という呪文も必要なくなるのです!

assertThat

様々なアサーションAPIが追加された一方、 assertThatJUnit 5では提供されていません。 JUnit 5の方針として、アサーションライブラリは開発者が好きなもの使ってね、というスタンスのようです。

assertThatを使いたい場合は、Hamcrestなどのサードパーティのライブラリを導入すればOKです!

構造化テスト

JUnit 4の @Enclosed の代わりに @Nested が導入されました。

public class EmployeeServiceTest {
    EmployeeService service;
    @BeforeEach
    void setUp() {
        this.service = new EmployeeService();
    }
    @Test
    @DisplayName("IDで検索する")
    void testFindById() {
        Employee employee = service.findById(1);
        assertAll("employee",
                () -> assertEquals("Midori", employee.getFirstName()),
                () -> assertEquals("Imai", employee.getLastName())
        );
    }

  //非static
    @Nested
    @DisplayName("名前で検索する")
    class FindByFirstNameStartingWithTest {
        @Test
        @DisplayName("firstNameが「E」から始まる")
        void startsWithE() {
      // アウタークラスのフィールドにアクセスできる
            List<Employee> employees = service.findByFirstNameStartingWith("E");
            assertEquals(employees.size(), 1);
            Employee employee = employees.get(0);
            assertAll("employee",
                    () -> assertEquals("Ema", employee.getFirstName()),
                    () -> assertEquals("Yasuhara", employee.getLastName())
            );
        }
    }
}

その違いは何と言っても、インナークラスが非staticになったことです! これによって、アウタークラスのプロパティを参照したり、前処理・後処理を共通化することができます。

まとめ

ここまでJUnit 5の新機能の一部を紹介してきました。 この他にも、パラメータ化テスト、タグ、Extensionなど 注目すべき機能がたくさんあります。 この機会にJUnit 5を是非触ってみてください! (できれば続き書きたいなあ。。)

Acroquest Technologyでは、キャリア採用を行っています。

  • ビッグデータHadoop/Spark、NoSQL)、データ分析(Elasticsearch、Python関連)、Web開発(SpringCloud/SpringBoot、AngularJS)といった最新のOSSを利用する開発プロジェクトに関わりたい。
  • マイクロサービスDevOpsなどの技術を使ったり、データ分析機械学習などのスキルを活かしたい。
  • 社会貢献性の高いプロジェクトや、顧客の価値を創造するようなプロジェクトで、提案からリリースまで携わりたい。
  • 書籍・雑誌等の執筆や、対外的な勉強会の開催・参加を通した技術の発信、社内勉強会での技術情報共有により、エンジニアとして成長したい。

  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 世の中に誇れるサービスを作りたいエンジニアwanted! - Acroquest Technology株式会社のエンジニア中途・インターンシップ・契約・委託の求人 - Wantedlywww.wantedly.com