ทำให้ JUnit กลับมายิ่งใหญ่อีกครั้ง มาใช้งาน JUnit 5 กันเถอะครับ
ถ้าพูดถึง Testing frameworks สำหรับ Java Virtual Machine (JVM) แล้ว testing framework ที่ได้รับความนิยมและเป็นที่รู้จักกันดี คงไม่มีใครที่ไม่รู้จัก JUnit ซึ่ง JUnit นั้นได้เริ่มต้นมาตั้งแต่ปี 1995 กันเลยทีเดียว จากนั้นมา JUnit ก็ได้เติบโตและได้รับความนิยมมาเรื่อยๆจนติดอันดับ 1 Java Libraries ในปี 2017 กันเลยทีเดียว
ในปี 2006 นั้น JUnit 4 ได้ release และได้มีการใช้งานมาอย่างยาวนาน แต่เมื่อปีที่แล้ว (กันยายน 2017) JUnit version 5.0.0 ก็ได้ถูก release ออกมา โดยในการ release ครั้งนี้ ได้มีการออกแบบ architecture ใหม่ครั้งใหญ่ โดย JUnit 5 นั้นจะเป็น modular framework ที่ประกอบไปด้วย components หลัก 3 ตัว คือ JUnit Platform, JUnit Jupiter, และ JUnit Vintage
JUnit 5 ยังมาพร้อมกับความสามารถอื่นๆอีกมากมาย และหนึ่งในความสามารถที่น่าสนใจที่พึ่งปล่อยออกมาพร้อมกับ JUnit version 5.3 ก็คือ parallel test execution ซึ่งผมเองก็ไม่รอช้าและได้ทำการ migrate project ที่ผมทำงานอยู่ไปเป็น JUnit 5 เรียบร้อย วันนี้เลยถือโอกาสมาเขียนบทความเกี่ยวกับการ migrate จาก JUnit 4 ไปเป็น JUnit 5 พร้อมกับนเสนอ feature ต่างๆของ JUnit 5 ไว้สักหน่อยครับ
What is JUnit 5?
ก่อนที่จะทำการไปใช้งาน JUnit 5 นั้น เรามาทำความรู้จักกันก่อนครับว่า JUnit 5 คืออะไร
JUnit 5 นั้น มีความแตกต่างจาก JUnit version ก่อนๆ โดย modules ต่างๆของ JUnit 5 มีความแตกต่างจาก JUnit 4 ซึ่ง Unit 4 ไม่ได้เป็น modular โดย JUnit 4 จะเป็น monolithic โดยทุกๆความสามารถของ JUnit 4 มาจาก junit.jar
แต่สำหรับ JUnit 5 จะ design ให้มีความเป็น modular โดยจะประกอบไปด้วย modules หลักๆ 3 ตัว คือ
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
Platform เป็น foundation สำหรับ launching testing frameworks
Jupiter เป็น programming model และ extension model
Vintage เป็น TestEngine สำหรับ running JUnit 3 และ JUnit 4
Tip
JUnit 5 ได้ชื่อ Jupiter เพราะมีชื่อ ขึ้นต้นด้วย JU และ Jupiter เป็นดาวเคราะห์ที่อยู่ห่างจากดวงอาทิตย์เป็นลำดับที่ 5
Moving from JUnit 4 to JUnit 5
ในการใช้งาน JUnit 5 นั้น เราสามารถเลือกใช้ตัว execute ได้หลายรูปแบบ ไม่ว่าจะใช้ build tools เช่น Maven หรือ Gradle หรือว่าใช้ IDE เช่น IntelliJ (ตั้งแต่ version 2016.2) หรือ Eclipse (ตั้งแต่ version 4.7, Oxygen) สำหรับ Gradle แล้ว ปัจจุบันนี้ JUnit 5 รองรับแบบ native เรียบร้อย สำหรับ Gradle version 4.6 ขึ้นไป แต่ในบทความนี้ผมจะใช้ maven เนื่องจากผมจะทำการ migrate จาก JUnit 4 ไปเป็น JUnit 5 จาก project ที่ผมมีอยู่แล้ว จากบทความ Unit Testing และ Integration testingใน Spring Boot มาเริ่มกันเลยครับ
Setup
เพื่อที่จะ run Jupiter ใน maven project นั้น เราต้องทำการ config pom.xml โดยเราต้องทำการเพิ่ม junit-jupiter-api module ดังนี้
อันดับแรกระบุ version ของ JUnit 5
<junit.jupiter.version>5.3.1</junit.jupiter.version>
<junit.platform.version>1.3.1</junit.platform.version>
จากนั้นทำการเพิ่ม JUnit 5 dependencies
และก็ทำการเพิ่ม maven-surefire-plugin
เพียงเท่านี้เราก็สามารถใช้ JUnit 5 ได้ด้วย vintage-engin (ตัวช่วยให้ run unit tests ที่ใช้ JUnit 4 หรือ JUnit 3 กับ Jupeter ได้) กับ maven project ของเราได้แล้วครับ ทำการทดลอง run unit tests ด้วย command
$ ./mvnw clean test
ก็จะได้ results ดังนี้
Writing Tests
หลังจากที่เราทำการ migrate จาก JUnit 4 ไปเป็น JUnit 5 ได้แล้ว ขั้นตอนถัดไป คือการ migrate unit testing ทั้งหมดของเราไป run ด้วย Jupiter
อันดับแรกในการ migrate test ของเราไปเป็น JUnit5 เราต้องทำการเปลี่ยน @RunWith annotation ที่เราใช้กับ class test ต่างๆมาเป็น @ExtendWith annotation
แทนที่
@RunWith(SpringRunner.class)
ด้วย
@ExtendWith(SpringExtension.class)
จากนั้น SpringExtension จะทำหน้าที่ในการ integrates ตัว Spring TestContext Framework กับ JUnit 5 ให้เรา
หลังจากที่เราได้แทนที่ @RunWith(SpringRunner.class) ด้วย @ExtendWith(SpringExtension.class) เรียบร้อยแล้ว เราต้องทำการเปลี่ยน package ของ @Test annotation จาก
org.junit.Test
ไปเป็น
org.junit.jupiter.api.Test
ดังนี้
นอกจากนั้น Class ไหนที่มีการใช้ Test lifecycle ด้วย ตอนนี้ Test lifecycle annotation ต่างๆ ของ JUnit 5 ก็มีการปรับเปลี่ยนต่างจาก JUnit 4 ซึ่งเราก็ต้องเปลี่ยนตามไปด้วย เมื่อมีการใช้งาน JUnit 5 ยกตัวอย่างที่เราใช้งานบ่อย ดังนี้
@Before, @After → @BeforeEach, @AfterEach@BeforeClass, @AfterClass → @BeforeAll, @AfterAll
โดยลำดับของการทำงานของ annotation ต่างๆจะเป็นดังนี้ครับ
หลังจากที่เราเปลี่ยน package ของ @Test พร้อมเปลี่ยน annotation ต่างๆ ไปเป็นของ JUnit 5 เรียบร้อยแล้ว ให้ทำการทดสอบ run unit tests ของเราอีกครั้ง
$ ./mvnw clean test
จะได้ results ดังนี้
Run your Unit Tests in Parallel
หลังจากที่เราทำการ migrate ไปใช้งาน JUnit 5 แล้ว มี feature อีกตัวที่น่าสนใจที่พึ่งเพิ่มเข้ามาใน JUnit 5 ตั้งแต่ version 5.3 นั้นก็คือ เราสามารถเปิดใช้งาน parallel execution ได้แล้ว โดยการใช้งานนั้นก็แสนจะง่ายมากๆ เพียงแค่เราทำการ set ค่า junit.jupiter.execution.parallel.enabled configuration เป็น True ใน junit-platform.properties file (ไฟล์นี้จะอยู่ที่ src/test/resources/junit-platform.properties)
หลังจากที่เรา set ค่า junit.jupiter.execution.parallel.enabled เป็น true แล้ว JUnit Jupiter engine จะทำการ execute tests เป็นแบบ parallel สำหรับการตั้งค่าอื่นๆ สามารถดูเพิ่มเติมได้ที่ documentation ของ JUnit 5 มาเริ่มกันเลยครับ
ทำการสร้างไฟล์ junit-platform.properties พร้อมกับ set ค่า ดังนี้
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=fixed
สำหรับค่า strategy นั้นขึ้นอยู่ว่าแต่ละคนจะตั้งค่าแบบไหน โดยสามารถดูเพิ่มเติมได้ที่ execution config document
หลังจากที่ทำการตั้งค่า configuration ต่างๆเสร็จแล้ว ก่อนที่จำทำการ run unit tests ผมก็จะทำการเพิ่ม
System.out.println("Thread: " + Thread.currentThread().getName());
ในทุกๆ test cases ที่มีอยู่ตอนนี้ จากนั้นก็ทำการ run unit tests
$ ./mvnw clean test
ถ้าเราลองสังเกตที่ console เราจะเห็น แต่ละ test case จะระบุ pool และ worker ที่ใช้ในการ run unit tests ถูก print ออกมา ดังนี้
Next Level With New JUnit 5 Features
ในส่วนนี้จะเป็น features ใหม่ๆของ JUnit 5 ที่จะช่วยให้เราเขียน unit testing ได้สะดวกมากยิ่งขึ้น
Skipping tests
ในการ skip test นั้น JUnit 5 จะใช้ @Disabled annotation (อยู่ใน package org.junit.jupiter.api) เราสามารถใช้ @Disabled ได้ทั้ง class level หรือ method level
@Disabled ระดับ class
@Disabled("All test in this class will be skipped")
class AllDisabledTest {
@Test
void skippedTestFoo() {
}
@Test
void skippedTestBar() {
}
}
@Disabled ระดับ method
class DisabledTest {
@Disabled
@Test
void skippedTest() {
}
}
Display names
@DisplayName annotation ใช้ในการ override ชื่อ test class หรือ method ด้วยการทำ custom message พร้อมสามารถใช้งาน special characters หรือ emojis ได้ด้วย
Assertions
สำหรับ assertion หรือที่เรารู้จักกันในอีกชื่อหนึ่งว่า predicate ซึ่งก็คือ boolean statement ที่เราใช้ในการตรวจสอบความถูกต้อง ซึ่งจะประกอบไปด้วย 3 ส่วนคือ
- ส่วนแรก expected value หรือว่า expected outputs
- ส่วนที่สอง คือ real outcome หรือว่าค่าจริงๆที่เป็น outputs
- และส่วนสุดท้าย คือ การเอา 2 ค่าด้านบนมา compare กันโดยการใช้งาน logic comparator แบบต่างๆ เช่น equals or not, higher or lower value
basic assertions
สำหรับ Jupiter assertions จะอยู่ใน class org.junit.jupiter.Assertions ซึ่งจะมี
basic assertions แบบต่างๆ เช่น
- assertTrue
- assertFalse,
- assertNull
- assertNotNull
- assertEquals
- assertArrayEquals
- assertIterableEquals
- assertLinesMatch
- assertNotEquals
- assertSame
- assertNotSame
assertAll
Jupiter assertions ที่สำคัญอีก 1 ตัว ของ Jupiter assertion ก็คือ assertAll
assertAll ช่วยให้เราสามารถใช้ group assertions แบบต่างๆในเวลาเดียวกันได้ โดยปกติเวลาเราทำการ assertion จำนวนหลายข้อ เมื่อ assertion ไหนเกิดการ failure เราก็จะรู้เฉพาะ failure แรกเท่านั้น แต่สำหรับ group assertion นั้น ทุก assertions จะถูก execute เสมอ ดังนั้นถ้าเกิด failures ก็จะถูก report ออกมาพร้อมๆกัน
Asserting exceptions
assertThrows เป็นอีก assertion ที่สำคัญใน Jupiter assertion เพราะ assertThrows ช่วยให้เราตรวจสอบ return exception และ verify message, cause และ stacktrace ได้อย่างง่าย
Asserting timeouts
ในการ assess timeouts ใน JUnit 5 tests นั้น เราสามารถใช้งาน assertTimeout ได้แล้ว เช่น
Conclusion
บทความนี้เราก็ได้ทำความรู้จักกับพื้นฐานของ JUnit 5 ซึ่ง JUnit 5 เองก็มี API เจ๋งๆ ที่ช่วยเราสามารถใช้ในการสร้าง test cases มากมาย เราได้รู้จัก basic annotation ต่างๆของ JUnit 5 พร้อมทำความรู้จักกับ assertions แบบต่างๆใน JUnit 5 เราได้ทดลอง migrate จาก JUnit 4 ไปเป็น JUnit 5 โดยใช้ existing project ซึ่งตรงนี้ผมเองก็หวังว่าจะเกิดประโยชน์กับคนที่สนใจอยากนำไปปรับใช้กับ project ที่เราทำงานจริงๆในชีวิตประจำวัน เพราะผมเองก็พึ่ง migrate existing project ที่ผมทำงานไปเป็น JUnit 5 เมื่อไม่นานมานี้ ซึ่ง parallel test execution ก็ช่วยให้ผมลดเวลาในการ runing unit tests แต่ละครั้งได้ 20%–30% เลยทีเดียว
JUinit 5 ยังมีอีกหลายอย่างที่บทความนี้ไม่ได้กล่างถึง เช่น filtering tests, conditional test execution, assumptions, nested tests,และ repeated tests สำหรับผู้ที่สนใจศึกษาต่อ สามารถดูได้จาก JUnit 5 User Guide