天天看點

快速入門單元測試-Junit5

快速入門單元測試-Junit5

1. 簡介

本文可以讓你快速了解Junit5一些特性,迅速掌握Junit5的一些概念。有些地方大家清楚特性就行,當我們使用到的時候,再去通過官網探究功能接口的詳細使用尤其是​

​其他​

​章節的部分。

2. 測試的結構

快速入門單元測試-Junit5

從這個測試案例我們開始介紹,看上圖的測試裡面我們可以介紹幾個相關的概念。

  • ​@Test​

    ​​注解表示這個方法是一個測試方法。使用IDE的支援,可以直接點選小圖示直接運作測試。值得一提的是,現在大部分的IDE和建構工具都對Junit5進行了支援。 ​

    ​@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));
    }
}      
快速入門單元測試-Junit5

​@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));
    }
}      
快速入門單元測試-Junit5

如果預設情況下使用非靜态的就會報錯

快速入門單元測試-Junit5

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...");
    }

}      

執行結果:

快速入門單元測試-Junit5

​​更多​​

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...");
    }


}      

執行結果:

快速入門單元測試-Junit5

更靈活詳細的用法,請參考官方文檔。

​​更多​​

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)));
    }      
快速入門單元測試-Junit5

測試是我們運作時動态生成的。

​​更多​​

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)​

​進行避免,原理看​​測試時的對象​​

快速入門單元測試-Junit5

類的層次結構:

快速入門單元測試-Junit5

這裡面的類都是測試類(​

​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(){
    }
    
}      
快速入門單元測試-Junit5

還有很多有意思的使用方式,比如使用的是​

​DisplayNameGenerator​

​, 可以自定義使用什麼方式生成測試的描述。比如 把測試類或者測試方法的名稱中下劃線替換成空格來當作測試的描述

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
 class A_year_is_not_supported {
    @Test
    void if_it_is_zero() {
      }
 }      
快速入門單元測試-Junit5

​​更多​​

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...");
    }
}      
快速入門單元測試-Junit5

還有一種情況,就是為某個接口的锲約寫測試,比如​

​Object.equals​

​​ or ​

​Comparable.compareTo​

​。遵守一定的規則,來測試他們到底遵守沒有遵守。可在接口使用測試

​​更多​​

9. 總結

測試的結構裡面有,​

​Test Class​

​​ ​

​Test Method​

​​ 、​

​Lifecycle Method​

​​、​

​斷言​

​。

​Test Class​

​是我們測試所在的類,其中@Nested修飾的class會讓測試更有表達性和層次,

​Test Method​

​是我們要運作的測試的方法,其中包含一些可多次執行的測試方法,執行可指定測試案例的測試方法

​Lifecycle Method​

​是我們在測試前後執行的方法,主要用于一些環境的準備和資源的釋放

斷言是判斷測試執行是否正确,我們也可以使用第三方庫的斷言讓判斷更有表達性。

我們思考分析了為什麼@BeforeAll和@AfterAll預設情況隻能修飾靜态方法,這和測試時生成的測試用例有關。Junit還有一些其他的特性,其中擴充讓我們在測試中根據特定的場景使用一些外部的擴充功能,更加靈活。

10. 參考