มาทำความรู้จัก JUnit 5 กันเถอะ
สำหรับ Java developer ส่วนใหญ่ ถ้าเขียน Unit test จะเคยใช้ JUnit สำหรับ Java application แต่ที่เราใช้อยู่เมื่อก่อนนี้ส่วนใหญ่จะเป็น JUnit 4 แต่มีเวอร์ชันล่าสุดที่เพิ่มประสิทธิภาพของ JUnit มากมาย นั้นก็คือ JUnit นั้นเอง ซึ่งเป้าหมายของเวอร์ชันนี้ก็คือรองรับ feature ต่างๆ ของ Java 8 ขึ้นไป รวมทั้งทำให้มีการ Test ในแบบต่างๆ เพิ่มขึ้นมากมาย
JUnit 5 คืออะไร?
ไม่เหมือนกับ JUnit เวอร์ชันก่อนหน้านี้ ซึ่ง JUnit 5 เป็นการรวมกันของส่วนประกอบหลัก 3 ส่วน คือ
JUnit 5 = JUnit Platfrom + JUnit Jupiter + JUnit Vintage
JUnit Platform
เป็นส่วนพื้นฐานของ JUnit ที่ทำหน้าที่เริ่มต้นกระบวนการทดสอบบน JVM โดยมีการกำหนด TestEngine
API สำหรับพัฒนา Testing Platform เพื่อมาทำงานบน JUnit Platform นี้อีกทีหนึ่งได้ นอกจากนี้แล้วยังสามารถเรียกใช้งานการทดสอบด้วยคำสั่งด้วย Console Launcher
และ JUnit 4 base Runner
สำหรับทำงาน TestEngine
ใน JUnit 4 environment ได้ด้วย ซึ่ง IDE หลักๆ อย่างเช่น Eclipse, IntelliJ หรือ VS Code ก็รองรับ JUnit 5 Platform ด้วย
JUnit Jupiter
เป็นการรวมกันของ programming model รูปแบบใหม่ ซึ่งมี annotation ใหม่ๆ มากมาย และ extension model สำหรับเขียนการทดสอบ (Test) และส่วนขยาย (Extension) ใน JUnit 5 ซึ่ง project ย่อยของ Jupiter จะมี TestEngine
สำหรับทำให้การทดสอบที่สร้างด้วย Jupiter ทำงานได้บน JUnit 5 Platform
JUnit Vintage
เป็นส่วนที่มี TestEngine
ที่ทำให้ JUnit 3 และ JUnit 4 ทำงานได้บน JUnit 5 Platform
Install
JUnit 5 รองรับ build tools และ dependency management ยอดนิยมต่างๆ ทั้งหมด เช่น Maven, Gradle หรือ Ant ซึ่งตัวอย่างของการเพิ่ม JUnit 5 สำหรับ Maven และ Gradle ดังนี้
Maven
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
Gradle
dependencies {
testImplementation('org.junit.jupiter:junit-jupiter:5.4.0')
}
test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
เริ่มต้นเขียน Test
เพื่อทดลองเขียน Test case ด้วย JUnit 5 จากตัวอย่าง (จากคู่มือ JUnit 5) ก็เลยลองเขียน Test case ง่ายๆ จะเห็นได้ว่า @Test
annotation จะ import มาจาก Jupiter package ซึ่งเป็นการใช้งานส่วนของ JUnit Jupiter ที่ทดสอบผลรวมของ 2 จำนวน ของ Calculator
class โดยใช้ assertEquals
assertion มาทดสอบค่าที่ได้จาก method add
import com.iphayao.demo.Calculator;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MyFirstJupiterTest {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
Annotation
ใน JUnit Jupiter มี Annotation ใหม่ๆ และที่มีอยู่แต่เปลี่ยนแปลงมากมาย ซึ่จะยกตัวอย่างของ Annotation ที่สำคัญดังนี้
@BeforeAll/@BeforeEach
ใน JUnit Jupiter แยก @Before
annotation ออกเป็น 2 annotation เพื่อจะ setup ก่อนทดสอบได้ 2 แบบ คือ @BeforeAll
ทำก่อนการทดสอบทั้งหมด และ @BeforeEach
ทำก่อนแต่ละการทดสอบ
@BeforeAll
static void setup() {
log.info("Execute before all test method");
}
@BeforeEach
void init() {
log.info("Execute before each test method");
}
@BeforeAll
annotation จำเป็นต้องเป็น static method และ @BeforeEach
จะต้องเป็น non static method
@AfterAll/@AfterEach
เช่นเดียวกับ @Before
ใน JUnit Jupiter แยก @After
ออกเป็น 2 annotation เพื่อที่จะดำเนินการหลังจากการทดสอบ คือ @AfterAll
ทำหลังจากการทดสอบทั้งหมด และ @AfterEach
ทำหลังแต่ละการทดสอบ
@AfterAll
static void done() {
log.info("Execute after all test method");
}
@AfterEach
void tearDown() {
log.info("Execute after each test method");
}
@AfterAll annotation จำเป็นต้องเป็น static method และ @AfterEach จะต้องเป็น non static method
@DisplayName/@Disable
ใน JUnit Jupiter ได้เพิ่ม @DisaplayName
annotation สำหรับแสดงชื่อของการทดสอบ หรือ Test method ที่แตกต่างจากชื่อของ test method เองเพื่อที่จะทำให้ชื่อของการทดสอบเข้าใจง่ายยิ่งขึ้น และ @Disable
annotation สำหรับปิด test method เพื่อที่จะไม่ต้องทำการทดสอบ test method นั้น สำหรับกรณีที่เราอาจจะไม่ต้องการทดสอบใน test method นั้นๆ
@Test
@DisplayName("Addition test successful")
void additionTest() {
assertEquals(2, calculator.add(1, 1));
}
@Test
@Disabled
void disableTest() {
assertEquals(3, calculator.add(1,1));
}
Assertion
Assertion ของ JUnit ย้ายมาใว้ใน package org.junit.jupiter.api.Assertions
และมีการพัฒนาเพิ่มเติมมากมากที่สำคัญคือการรองรับ feature ของ Java 8 ที่ทำให้ใช้ rambdas ใน assertion ได้เลย
@Test
void groupAssertion() {
assertAll("person",
() -> assertEquals("John", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}@Test
void dependencyAssertion() {
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n"))
);
}
);
}
ทำให้การทดสอบที่เขียนด้วย JUnit 5 สามารถมี assertion ที่ซับซ้อนได้ง่ายมากขึ้น
Assumption
จะใช้ Assumption กับการทดสอบที่ต้องตรวจสอบเงื่อนไขที่เป็นจริง โดยใช้สำหรับเงื่อนไขภายนอกที่จะทำการทดสอบ สามารถใช้ assumption ด้วย assumeTrue()
, assumeFalse()
และ assumingThat()
และสามารถใช้ rambdas expression ได้
@Test
void testOnlyOnProductionServer() {
assumeTrue("DEV".equals(System.getenv("ENV")), () -> "Aborting test: not on developer workstation");
assertEquals(2, calculator.add(1, 1));
}
@Test
void testOnlyOnCiServer() {
assertFalse("CI".equals(System.getenv("ENV")));
assertEquals(2, calculator.add(1, 1));
}
@Test
void testAllEnvironment() {
assumingThat("CI".equals(System.getenv("ENV")), () -> {
assertEquals(2, calculator.add(1, 1));
});
assertEquals(4, calculator.multiply(2, 2));
}
Exception
ใน JUnit 5 ได้เพิ่มให้รองรับ exception ได้ดีขึ้น ด้วย assertThrows()
method มาตรวจสอบว่ามี exception เกิดขึ้นหรือไม่
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class,
() -> calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
และทำให้สามารถตรวจสอบข้อมูลของ exception ได้ด้วย
Dynamic Test
ใน JUnit 5 ได้เพิ่ม Dynamic Test feature ที่ทำให้เราสามารถประกาศและสร้างการทดสอบได้ในขณะ run-time ซึ่งตรงกันข้ามกับ Static Test ที่จะต้องระบุการทดสอบก่อนที่จะทำการทดสอบที่ compile-time
Dynamic Test สามารถสร้างได้ด้วย @TestFactory
annotation และก็ใช้ rambdas expression ได้เช่นเดียวกัน
@TestFactory
Stream<DynamicTest> dynamicTestFromStream() {
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n,
() -> assertTrue(n % 2 == 0)));
}
Repeat Test
ถ้าต้องทดสอบการทดสอบเดิมซ้ำๆ ใน JUnit 5 ก็มี feature รองรับความต้องการนี้ด้วย @RepeatTest
annotation ทำให้เราสามารถทดสอบการทดสอบเดิมซ้ำได้ตามที่ต้องการ
@RepeatedTest(10)
void additionTest() {
assertEquals(2, calculator.add(1, 1));
}
Nested Test
@Nested
annotation ทำให้ developer สร้างการทดสอบที่มีความซับซ้อนด้วยการซ้อนการทดสอบไว้ในอีกการทดสอบได้ ที่เป็นการทดสอบที่มีความสัมพันธ์กัน
public class TestStackDemo {
Stack<Object> stack;
@Test
void isInstantiateWithNew() {
new Stack<>();
}
@Nested
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Nested
class AfterPushing {
String element = "an element";
@BeforeEach
void pushAnElement() {
stack.push(element);
}
@Test
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
}
}
}
Parameterized Test
ใน JUnit 5 สามารถจะทำการทดสอบหลายครั้ง ซึ่งแต่ละครั้งมีค่าของการทดสอบที่แตกต่างกันได้ด้วย @ParameterizeTest
annotation โดยกำหนดค่าของการทดสอบด้วย @ValueSource
และอีกหลาย annotation (@NullSource
, @EmptySource
, @NullAndEmptySource
, @EnumSource
เป็นต้น)
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4})
void testWithValueSource(int arg) {
assertTrue(arg > 0 && arg < 4);
}
สรุป
ในบทความนี้เป็นการแนะนำให้รู้จัก JUnit 5 ซึ่งเป็นเวอร์ชั่นใหม่ล่าสุดของ JUnit framework สำหรับ ชาว Java developer ทั้งหลาย โดยทีความสามารถที่เพิ่มขึ้นอีกมากมาย ซึ่งในบทความนี้เป็นเพียงแค่บางส่วนของความสารถทั้งหมดของ JUnit 5 ที่จะทำให้สร้างการทดสอบได้ง่ายและดียิ่งขึ้น เพื่อท้ายที่สุดจะทำให้ซอฟต์แวร์ที่เราสร้างนั้นมีคุณภาพสูงสุด