GMOアドマーケティングのT.Nです。
弊社のプロジェクトのJUnitを、JUnit 5にアップグレードしたので、
今回はJUnit 5について書きます。
JUnit 5とは
JUnit 5 User Guideには以下のように記載されています。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
引用元: JUnit 5 User Guide
それぞれを簡単に説明すると、以下のようになります。
JUnit Platform | テスト実行のためのプラットフォームなどの部分 |
JUnit Jupiter | JUnit 5のテストを書くためのAPIやテストエンジン |
JUnit Vintage | JUnit3、JUnit 4のテストエンジン |
JUnit 4では、全ての機能が1つのパッケージに入っていましたが、
JUnit 5からは分割されました。
JUnit 5では、必要に応じてJARファイルを依存関係に含める必要があります。
JUnit 4で書かれたテストを実行する場合は、junit-vintage-engineが必要になります。
現時点では、experimentalの機能も別のパッケージで提供されています。
JUnit 5を使用したテスト
JUnit 5の特徴を活かしたテストを紹介します。
今回は、JSONなどをオブジェクトに変換する、Jacksonのテストクラスを部分的に作成しました。
テストでは、ObjectMapperTesterというインターフェースに記述した内容が、
JSON、YAML、XMLの場合でそれぞれ実行されるようになっています。
テストクラス
ObjectMapperTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.MappingJsonFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import models.Employee; import org.junit.jupiter.api.*; import java.io.IOException; import java.nio.file.Paths; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.junit.jupiter.api.DynamicTest.dynamicTest; @DisplayName("ObjectMapperのテスト") class ObjectMapperTest { interface ObjectMapperTester { ObjectMapper createObjectMapper(); String getFilePath(); JsonFactory expectedJsonFactory(); @TestFactory @DisplayName("readValueでFileからEmployeeオブジェクトに変換できること") default Stream<DynamicNode> readValueByFile() throws IOException { ObjectMapper mapper = createObjectMapper(); Employee employee = mapper.readValue(Paths.get(getFilePath()).toFile(), Employee.class); return Stream.of( dynamicTest("idが1であること", () -> assertEquals(1, employee.getId())), dynamicTest("nameがGMOであること", () -> assertEquals("GMO", employee.getName())), dynamicTest("ageが24であること", () -> assertEquals(24, employee.getAge())) ); } @Test @DisplayName("getFactoryで期待した内容を取得できること") default void getFactory() { ObjectMapper mapper = createObjectMapper(); assertEquals(expectedJsonFactory().getClass(), mapper.getFactory().getClass()); } } @Nested @DisplayName("JSONの場合") class Json implements ObjectMapperTester { @Override public ObjectMapper createObjectMapper() { return new ObjectMapper(); } @Override public String getFilePath() { return "src/test/resources/json/employee.json"; } @Override public JsonFactory expectedJsonFactory() { return new MappingJsonFactory(); } } @Nested @DisplayName("YAMLの場合") class Yaml implements ObjectMapperTester { @Override public ObjectMapper createObjectMapper() { return new YAMLMapper(); } @Override public String getFilePath() { return "src/test/resources/yaml/employee.yaml"; } @Override public JsonFactory expectedJsonFactory() { return new YAMLFactory(); } } @Nested @DisplayName("XMLの場合") class Xml implements ObjectMapperTester { @Override public ObjectMapper createObjectMapper() { return new XmlMapper(); } @Override public String getFilePath() { return "src/test/resources/xml/employee.xml"; } @Override public JsonFactory expectedJsonFactory() { return new XmlFactory(); } } } |
テストで使用したEmployeeクラス
Employee.java
1 2 3 4 5 6 7 8 9 10 |
package models; import lombok.Data; @Data public class Employee { private int id; private String name; private int age; } |
テストで使用したデータ
employee.json
1 2 3 4 5 |
{ "id": 1, "name": "GMO", "age": 24 } |
employee.yaml
1 2 3 |
id: 1 name: "GMO" age: 24 |
employee.xml
1 2 3 4 5 |
<employee> <id>1</id> <name>GMO</name> <age>24</age> </employee> |
テストクラスの解説
上記のテストクラスでは、JUnit 5の以下のような特徴を取り入れています。
Test Interfaces and Default Methods
インターフェースに@Testなどをつけたdefaultメソッドを定義することで、
インターフェースを実装したクラスで、テストを実行することができます。
先ほどのテストクラスでは、
変換前のデータ形式ごとに内部クラスを作成し、インターフェースを実装しています。
それぞれの内部クラスでは、インターフェースに定義された、
データ形式ごとのObjectMapperを取得するメソッドや、
expectedの値を取得するメソッドなどをオーバーライドしています。
インターフェースにテストを記述することで、
テストを共通化できるので、テストコードの量を減らすことができます。
また、defaultメソッドに、
@BeforeAll、@BeforeEach、@AfterAll、@AfterEachなどのアノテーションをつけることで、
インターフェースを実装したクラスでの前処理、後処理を定義することもできます。
Dynamic Tests
JUnit 5ではテストを動的に実行することができます。
Dynamic Testは、@TestFactoryをつけた、Streamを返すメソッドによって定義することができます。
テストデータを動的に生成する場合などに活用できそうです。
先ほどのテストクラスでは、Dynamic Testのメリットを十分に活かせていないかもしれませんが、
JUnit 5を使ったテストクラスでは、活用する機会も多くなると思います。
Nested Tests
JUnit 5ではテストをネストさせることができます。
@Nestedをつけた内部クラスによって定義することができます。
先ほどのテストクラスでも、
テストケースごとに内部クラスを定義し、テストケースが明確になるようにしました。
ケースごとにネストさせることで、
テストの内容が分かりやすいテストクラスを作成できると思います。
Display Names
@DisplayNameを使用して、テストクラスに説明を記述できます。
記述した内容が、テスト実行時に表示されます。
JUnit 5以前では、メソッド名にテストの説明が日本語で書かれることもあったと思いますが、
JUnit 5では、メソッド名は英語のままで、@DisplayNameにテストの説明を書くと良さそうです。
最後に
今回紹介したJUnit 5の機能は一部です。
他の機能にも興味をお持ちの方は、JUnit 5 User Guideをご覧になってください。
User Guideにはテストコードのサンプルがいくつか載っていますが、
どれもJUnit 5の特徴を簡単に説明するためのコードであったため、
今回のブログでは、より業務で使用するものに近そうなサンプルを載せてみました。
弊社でもJUnit 5にバージョンアップしたばかりなので、
どのようにJUnit 5でテストクラスを書くと良いかを模索している状況です。
まだ活用できていない機能もあると思いますが、
今後もJUnit 5を活用して、読みやすいテストクラスを作成していきたいです。