快速入門單元測試-Junit5
1. 簡介
本文可以讓你快速了解Junit5一些特性,迅速掌握Junit5的一些概念。有些地方大家清楚特性就行,當我們使用到的時候,再去通過官網探究功能接口的詳細使用尤其是
其他
章節的部分。
2. 測試的結構
從這個測試案例我們開始介紹,看上圖的測試裡面我們可以介紹幾個相關的概念。
-
注解表示這個方法是一個測試方法。使用IDE的支援,可以直接點選小圖示直接運作測試。值得一提的是,現在大部分的IDE和建構工具都對Junit5進行了支援。 @Test
就是本測試類的測試方法運作之前進行運作,一般用于測試環境的準備@BeforeAll
- 測試方法和所在的測試類不能是私有,其他的都可以。
- 斷言:判斷執行結果是否正确,比如:
assertEquals
通過上面的案例我們來做幾個定義
Lifecycle Method
: 生命周期方法 一般用于測試用例前或者後執行的方法,比如
@BeforeAll
,
@AfterAll
,
@BeforeEach
, or
@AfterEach
.上面的
setUp
就是一個
Lifecycle Method
。
Test Class
:包含測試方法的類,可以是頂層類、靜态成員類、或者Nested類。這些類不能是抽象且必須有構造方法。上面的
MyFirstJUnitJupiterTests
就是一個
Test Class
Test Method
:可以當作測試案例執行的方法。上面的方法
addition
就是一個
Test Method
3. Lifecycle Method
對測試的環境的準備必不可少,Junit提供了四個注解修飾方法,讓我們使用來對我們測試進行準備。
@BeforeEach
表示該方法在目前測試類(Test Class)的每個測試方法(Test Method)運作之前執行一遍,必須是非靜态方法
@AfterEach
表示該方法在目前測試類(Test Class)的每個測試方法(Test Method)運作完成之後執行一遍,必須是非靜态方法。
@BeforeAll
表示的是該方法應當在目前測試類(Test Class)中所有測試方法(Test Method)之前執行,預設情況下該方法必須是靜态(static)的。
@AfterAll
表示的是該方法應當在目前測試類(Test Class)中所有測試方法(Test Method)執行完成之後執行,預設情況下該方法必須是靜态(static)的。
用一個案例進行說明
class LifecycleMethodTests {
@BeforeAll
static void initAll() {
println("init all test...");
}
@BeforeEach
void init() {
println("init each test...");
}
@Test
void test_1() {
println("execute test 1");
}
@Test
void test_2() {
println("execute test 2");
}
@AfterEach
void tearDown() {
println("tear down each test...");
}
@AfterAll
static void tearDownAll() {
println("tear down all test...");
}
static void println(String message){
System.out.println(message);
}
}
執行結果為:
init all test...
init each test...
execute test 1
tear down each test...
init each test...
execute test 2
tear down each test...
tear down all test...
4. 測試時的執行個體
有沒有想過,
@BeforeAll
和
AftereAll
修飾的方法為什麼是靜态的?
Junit5考慮到測試方法之間獨立性和避免人為的修改狀态對測試造成副作用。Junit為每一個測試方法建立一個測試類的執行個體。
public class TestInstanceDemo {
private final Calculator calculator = new Calculator();
@BeforeAll
static void setUp(){
System.out.println("start to test....");
}
@Test
void addition() {
System.out.println(this);
assertEquals(2, calculator.add(1, 1));
}
@Test
void division () {
System.out.println(this);
assertEquals(2, calculator.divide(4, 2));
}
}
@BeforeAll
和
AftereAll
的目的測試方法共享一些什麼,不同執行個體共享的話,就隻能是類的狀态了,隻能是靜态方法了。
我們可以通過
@TestInstance
注解,來讓測試方法在統一執行個體下進行測試。---->
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
TestInstance.Lifecycle.PER_CLASS
表示用一個此類的執行個體測試下面的測試方法,這時
@BeforeAll
和
AftereAll
就可以修飾非靜态的方法了。
TestInstance.Lifecycle.PER_METHOD
表示每一測試方法都有一個不同的目前測試類的執行個體(預設情況)。
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestInstanceDemo {
private final Calculator calculator = new Calculator();
@BeforeAll
void setUp(){
System.out.println("start to test....");
}
@Test
void addition() {
System.out.println(this);
assertEquals(2, calculator.add(1, 1));
}
@Test
void division () {
System.out.println(this);
assertEquals(2, calculator.divide(4, 2));
}
}
如果預設情況下使用非靜态的就會報錯
5. Test Method
測試方法,用于測試某個接口,直接可以運作的方法。
最簡單最常用的是‘
5.1 Test
@Test修飾的方法為測試方法
示例:
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
5.2 Repeated Test
@RepeatedTest修飾的方法為測試方法。
這個是為了指定測試方法的執行次數。每一次執行效果就和@Test作用一樣
public class RepeatedTestsSimpleDemo {
@BeforeEach
void beforeEach() {
System.out.println("init each test...");
}
@RepeatedTest(3)
void repeatedTest() {
System.out.println("test...");
}
@AfterEach
void tearDown() {
System.out.println("tear down each test...");
}
}
執行結果:
更多
5.3 Parameterized Test
@ParameterizedTest修飾的方法為測試方法。
此注解可以讓我們的測試方法執行多次,但是可以使用不同的參數。必須配合
@ValueSource
注解。
public class ParameterizedTestDemo {
@BeforeEach
void beforeEach() {
System.out.println("init each test...");
}
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
System.out.println("test...");
assertTrue(StringUtils.isNotBlank(candidate));
}
@AfterEach
void tearDown() {
System.out.println("tear down each test...");
}
}
執行結果:
更靈活詳細的用法,請參考官方文檔。
更多
5.4 Dynamic Test
我們的上面測試,在編譯完成之後就是已經是固定的了。現在我們需要在運作時候,生成我們的測試。
這時候我們使用**@TestFactory**注解,方法傳回一個
DynamicNode
或者是它的
Stream
、
Collection
、
Iterable
,、
Iterator
,運作此方法就會生成我們一個個的測試并且執行。有點像生成測試的工廠,是以注解名字是
TestFactory
舉例:
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
測試是我們運作時動态生成的。
更多
5.5 TestTemplate
不常用,略
更多
6. Nested Tests
為了更好描述測試之間的關系,比如有時候我們的測試的條件狀态遞進的,可以想象成一個狀态樹,每一個節點都有自己的測試狀态。這樣我們可以利用
@Nested
注解和java的
NestClass
,來更好描述描述測試層次結構。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
@Nested修飾的類必須是非靜态的内部類。預設上層的且同一條路線上的
@BeforeEach
and
@AfterEach
修飾的方法有效于下層的測試。預設情況下@Nested無中的@BeforeAll和@AfterAll無效,這是java本身的限制(java16之前不能在内部類裡面有靜态成員)。可使用
@TestInstance(Lifecycle.PER_CLASS)
進行避免,原理看測試時的對象
類的層次結構:
這裡面的類都是測試類(
Test Class
)。
更多
7. 斷言
斷言之前我們描述過,就是判斷我們的測試的執行結果是否正确
7.1 基本使用
Junit5本身提供了斷言,來讓我們進行判斷測試結果是否正确。
assertEquals
判斷相等,如果不相等則失敗。
assertEquals(2, calculator.add(1, 1));
assertTrue
:判斷結果為
True
,如果不為True則失敗。
assertTrue('a' < 'b')
assertNotNull
:判斷結果不為空,如果為空則失敗。
assertNotNull(firstName);
assertTimeout
:判斷逾時,如果逾時,則失敗。
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
分組斷言,就是把幾個斷言放在一個分組中
assertAll("person",
() -> assertEquals("Jane", person.getFirstName(),"12"),
() -> assertEquals("Doe", person.getLastName(),"12")
);
如果失敗了,設定失敗的描述資訊:
assertEquals(5, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
org.opentest4j.AssertionFailedError: The optional failure message is now the last parameter ==>
Expected :5
Actual :4
在某些測試情況,我們功能子產品是要抛出異常的,這也是功能的一部分,是以我們也需要對異常進行測試
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
7.2 三方庫的斷言
Junit的斷言,有時候會讓我們混淆哪裡是期望值,那裡是實際結果,比如
assertEquals(2, calculator.add(1, 1));
我們就比較容易混淆哪裡放期望值哪裡放實際結果值。
還有就是比對條件不夠靈活且描述性也不高。
是以我們可以使用第三方來進行補充,我們來看一個
harmast
來提供的斷言,比對條件。
@Test
void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}
我們就一眼看出,第一個參數是實際執行的結果,第二個參數就是對比對條件的描述,可以看出第二參數描述性很強。
7.3 Void方法怎麼測試
Void方法測試,我們可以使用
Mockito
架構,這是一個模拟依賴對象的架構,比如A模型依賴B模型,對A進行測試的時候,我們由于某些原因難以建構B模型,我們可以使用Mockito進行模拟一個B模型,來對A功能接口進行測試。
我們可以通過對模拟對象的一些接口的調用判斷來測試我們的無傳回值的接口。
執行個體:
@Test
void testAddItem(){
B b = mock(B.class);
A a = new A(b);
service.doSomthing(parameter);
verify(b).doSomething(any());
}
這樣就可以測試到我們的功能實作了,但這也測試到了實作細節。測試到實作細節,有一個後果就是實作世界是不問穩定,同這樣的功能今天這麼麼實作,明天那麼實作,這就導緻我們測試也得跟着改變。是以盡量少用verify。
更多
8. 其他
8.1 更好的描述
junit5 給我提供一個注解,來讓我們更好的描述我們的測試類的測試方法,預設使用的是測試方法名稱或者測試類的名稱
@DisplayName
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
@Test
void testDefault(){
}
}
還有很多有意思的使用方式,比如使用的是
DisplayNameGenerator
, 可以自定義使用什麼方式生成測試的描述。比如 把測試類或者測試方法的名稱中下劃線替換成空格來當作測試的描述
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
@Test
void if_it_is_zero() {
}
}
更多
8.2 組合注解(Composed Annotations)
Junit提供的注解都是元注解(
Meta-Annotations
),我們可以使用這些元注解組合自己的注解形成組合注解。組合注解的語義繼承這些元注解。
組合注解可以提高效率和可讀性
比如我們不使用組合注解寫法如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
@Fast
@Test
void myFastTest() {
// ...
}
使用之後
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
@FastTest
void myFastTest() {
// ...
}
上面是一樣的效果。
8.3不測試、某種條件下測試
不測試,
由于某些原因不進行測試,使用@Disable
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
class DisabledTestsDemo {
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
更多
某種條件下進行測試
這種條件可以是作業系統
@EnabledOnOs
、可以是java運作時環境
EnabledOnJre
、還可以是某個系統屬性
@EnabledIfSystemProperty
,當然也可以自定義
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
更多
8.4 測試的執行順序
測試之間是互相獨立的,是以測試的順序不影響結果,但是測試順序有時候也相當重要,尤其是內建測試的時候,比如對性能的影響等。Junit支援測試順序的設定。相關注解
@Order
@TestClassOrder
更多
8.5 設定逾時時間
**@Timeout(5) ** 可以添加到
Lifecle Method
和
Test Method
上,執行的過程中,逾時就報異常
更多
8.6 擴充(Extension)
如果Junit提供的接口,大部分都是注解,不能很好進行測試。我們可以自己通過擴充來實作自己想要的功能,例如Spring提供的
SpringExtension
,
使用方式
@ExtendWith(CertainExtension.class)
,Junit也提供了一些内置擴充
内置擴充
擴充模型
8.7 測試中使用接口和預設方法
@Test
,
@RepeatedTest
,
@ParameterizedTest
,
@TestFactory
,
@TestTemplate
,
@BeforeEach
, and
@AfterEach
都可以修飾inteface中的default方法。
繼承接口的類,也會把這些繼承過去。
以列印測試日志場景舉例
interface TestLifecycleLogger {
Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
static void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
static void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
public class TestLifecycleLoggerImplDemo implements TestLifecycleLogger{
@RepeatedTest(10)
void repeatedTest() {
System.out.println("test...");
}
}
還有一種情況,就是為某個接口的锲約寫測試,比如
Object.equals
or
Comparable.compareTo
。遵守一定的規則,來測試他們到底遵守沒有遵守。可在接口使用測試
更多
9. 總結
測試的結構裡面有,
Test Class
Test Method
、
Lifecycle Method
、
斷言
。
Test Class
是我們測試所在的類,其中@Nested修飾的class會讓測試更有表達性和層次,
Test Method
是我們要運作的測試的方法,其中包含一些可多次執行的測試方法,執行可指定測試案例的測試方法
Lifecycle Method
是我們在測試前後執行的方法,主要用于一些環境的準備和資源的釋放
斷言是判斷測試執行是否正确,我們也可以使用第三方庫的斷言讓判斷更有表達性。
我們思考分析了為什麼@BeforeAll和@AfterAll預設情況隻能修飾靜态方法,這和測試時生成的測試用例有關。Junit還有一些其他的特性,其中擴充讓我們在測試中根據特定的場景使用一些外部的擴充功能,更加靈活。