JUnit 5入门

1,073 阅读7分钟

这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战

概况

JUnit是一款优秀的开源Java单元测试框架,也是目前使用率最高最流行的测试框架,开发工具Eclipse和IDEA对JUnit都有很好的支持,JUnit主要用于白盒测试和回归测试。

相关词汇释义:

单元测试:单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。 其中,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

白盒测试:白盒测试又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试是一种测试用例设计方法,盒子指的是被测试的软件,白盒指的是盒子是可视的,即清楚盒子内部的东西以及里面是如何运作的。 "白盒"法全面了解程序内部逻辑结构、对所有逻辑路径进行测试。 "白盒"法是穷举路径测试。在使用这一方案时,测试者必须检查程序的内部结构,从检查程序的逻辑着手,得出测试数据。贯穿程序的独立路径数是天文数字。

回归测试:回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。 自动回归测试将大幅降低系统测试、维护升级等阶段的成本。

相关使用详见 JUnit 5官方文档中文版

基本使用

Spring Boot 本身集成了JUnit,在Maven中的体现如下:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

具体测试代码可在 src/test/java/项目名/下新建java文件编写

package com.example.demo;
​
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
​
// 标志为Spring Boot测试类
@SpringBootTest
public class DemoTests {
​
​
    // 在整个测试前执行,必须为static方法 
    @BeforeAll
    static void initAll(){
        System.out.println("BeforeAll init succeed!");
    }
​
    // 在每个测试方法前执行
    @BeforeEach
    void initEach(){
        System.out.println("--------------------------------------");
        System.out.println("BeforeEach init succeed!");
    }
​
    // 标识为测试方法
    @Test
    // 测试方法显示名
    @DisplayName("java method test demo1")
    void calculateTest1(){
        int actual = 1 + 2;
        // 断言,判断是否相等,是则继续执行
        Assertions.assertEquals(3, actual);
        System.out.println(actual);
    }
​
    @Test
    @DisplayName("java method test demo2")
    void calculateTest2(){
        int actual = 1 + 2;
        // 断言,判断是否相等,否则抛出错误org.opentest4j.AssertionFailedError
        Assertions.assertEquals(5, actual);
        System.out.println(actual);
    }
​
    // 每个测试方法后执行
    @AfterEach
    void afterEachDestroy(){
        System.out.println("AfterEach destroy succeed!");
        System.out.println("--------------------------------------");
    }
​
    // 整个测试后执行,必须为static方法 
    @AfterAll
    static void afterAllDestroy(){
        System.out.println("AfterAll destroy succeed!");
    }
​
}

运行结果如下:

image-20210730141951701

Demo1:

image-20210730142056077

Demo2:

image-20210730142248729

断言

编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。程序员相信在程序中的某个特定点该表达式值为真,可以在任何时候启用和禁用断言验证,因此可以在测试时启用断言而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新启用断言。

使用断言可以创建更稳定、品质更好且 不易于出错的代码。当需要在一个值为FALSE时中断当前操作的话,可以使用断言。单元测试必须使用断言(Junit/JunitX)。

具体使用方法如下所示,除了assertEquals()方法,还有assertAll()assertTimeout()等方法,详见官方文档。

    // 标识为测试方法
    @Test
    // 测试方法显示名
    @DisplayName("java method test demo1")
    void calculateTest1(){
        int actual = 1 + 2;
        // 断言,判断是否相等,是则继续执行
        Assertions.assertEquals(3, actual);
        System.out.println(actual);
    }
​
    @Test
    @DisplayName("java method test demo2")
    void calculateTest2(){
        int actual = 1 + 2;
        // 断言,判断是否相等,否则抛出错误org.opentest4j.AssertionFailedError
        Assertions.assertEquals(5, actual);
        // 不执行
        System.out.println(actual);
    }

假设

在org.junit.jupiter.api.Assumptions 中,封装了一组使用的方法,以支持基于假设的条件测试执行。

假设实际就是指定某个特定条件,假如不能满足假设条件,假设不会导致测试失败,只是终止当前测试。 这也是假设与断言的最大区别,因为对于断言而言,会导致测试失败。

个人理解断言一般用于测试判断结果是否正确,而假设更强调判断测试前置条件是否正确,若不正确,则测试没有必要继续进行下去了,即跳过该测试。

package com.example.demo;
​
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
​
import static org.junit.jupiter.api.Assumptions.assumeTrue;
​
@SpringBootTest
public class AssumeTest {
​
    @Test
    @DisplayName("assumeTestDemo1")
    void assumeTestDemo1(){
        assumeTrue(true);
        System.out.println("assume true!");
    }
​
    @Test
    @DisplayName("assumeTestDemo2")
    void assumeTestDemo2(){
        System.out.println("assume false!");
        assumeTrue(false);
        System.out.println("assume false!");
    }
​
​
}

运行结果如下:

Demo1:

image-20210730155248533

Demo2:

image-20210730155337583

再测试一段代码,加深理解。

package com.example.demo;
​
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
​
import static org.junit.jupiter.api.Assumptions.assumeTrue;
​
@SpringBootTest
public class AssumeTest {
​
    @BeforeAll
    static void beforeAssumeTest(){
        assumeTrue(false);
    }
​
​
    @Test
    @DisplayName("assumeTestDemo1")
    void assumeTestDemo1(){
        assumeTrue(true);
        System.out.println("assume true!");
    }
​
    @Test
    @DisplayName("assumeTestDemo2")
    void assumeTestDemo2(){
        assumeTrue(false);
        System.out.println("assume false!");
    }
​
}
​

执行结果如下:

image-20210730155741688

在@BeforeEach中同理。

禁用

禁用采用注解@Disabled实现,可用于测试类和测试方法,被标记的类或方法将不会执行。

package com.example.demo;
​
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
​
@SpringBootTest
public class DisabledTest {
​
    @Disabled
    @Test
    void disabledTestDemo1(){
        System.out.println("disable1");
    }
​
    @Test
    void disabledTestDemo2(){
        System.out.println("disable2");
    }
}
​

执行结果如下:

image-20210730162649718

标签

JUnit可以通过@Tag注解实现按标签进行测试。

示例代码如下:

TagTest1.java

@SpringBootTest
public class TagTest1 {
    
    @Test
    @Tag("hello")
    void TagTest1(){
        System.out.println("hello1");
    }
    
    @Test
    @Tag("hi")
    void TagTest2(){
        System.out.println("hi1");
    }
}

TagTest2.java

@SpringBootTest
public class TagTest2 {
​
    @Test
    @Tag("hello")
    void TagTest3(){
        System.out.println("hello2");
    }
​
    @Test
    @Tag("hi")
    void TagTest4(){
        System.out.println("hi2");
    }
​
}

筛选标签“hello”,执行结果如下:

image-20210802095105757

可见只有标签为“hello”的测试方法被执行。

测试实例生命周期

JUnit 5的默认测试实例生命周期为per-method,即对于每个测试方法,都重新实例化一个测试类。这就是为什么@BeforeAll和@AfterAll修饰的方法需要为静态方法。

但测试实例的生命周期可以通过@TestInstance(Lifecycle.PER_CLASS)修改成在同一个测试实例上执行所有测试方法。当使用这种模式时,每个测试类将创建一个新的测试实例。因此,如果您的测试方法依赖于存储在实例变量中的状态,则可能需要在@BeforeEach@AfterEach方法中重置该状态。

嵌套测试

当一个测试类有大量功能需要测试时,测试类的结构将变得繁杂。此时我们可以借助注解@Nested和内部类的形式优化测试类的结构,使测试展示更清晰

@DisplayName("A stack")
public 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());
            }
        }
    }
}

执行结果如下:

image-20210802110139273

重复测试

通过注解@RepeatTest可实现重复测试。

示例代码如下:

public class RepeatTest {
    // 该测试方法执行10次
    @RepeatedTest(10)
    void repeatTest(){
        System.out.println("Hello world!");
    }
}

执行结果如下:

image-20210802110917494

条件测试

JUnit 5支持条件测试,常见的条件注解有自定义条件@EnabledIf@DisabledIf和内置条件@EnabledOnOs

示例代码如下:

public class IfTest {
​
    @Test
    @EnabledIf("getCondition")
    void enabledIfTest() {
        System.out.println("Hello world1");
    }
​
    @Test
    @DisabledIf("getCondition")
    void disabledIfTest(){
        System.out.println("Hello world2");
    }
​
    boolean getCondition(){
        return true;
    }
}

执行结果如下:

image-20210802115152879

参数化测试

参数化测试是指每次使用不同参数进行执行测试方法,支持数组类型和Enum类型的参数注入,分别借助注解@ValueSourceEnumSource。需要注意的是,测试方法不再使用@Test注解,而是需要使用注解@ParameterizedTest

示例代码如下:

public class ParameterTest {
​
    @ParameterizedTest
    @ValueSource(strings = {"Lei Li","Meimei Han"})
    void parameterTest(String word){
        System.out.println("Hello,"+word);
    }
}

执行结果如下:

image-20210802112017274

进阶用法

Web服务测试

当对测试类标注注解@SpringBootTest的时候,测试时将先自动初始化SpringBoot环境,然后借助实例化对象TestRestTemplate即可进行Web服务的测试。

示例代码如下:

HelloController.java

@RestController
public class HelloController {
​
    @RequestMapping(value = "/hello",method = RequestMethod.GET)
    public Object hello(){
        return "hello world!";
    }
​
}

HelloControllerTests.java

// 标识测试环境为随机端口
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerTests {
​
    @Autowired
    private TestRestTemplate restTemplate;
​
    @Test
    @DisplayName("Web service test demo")
    void helloTest(){
        String object = restTemplate.getForObject("/hello", String.class);
        System.out.println(object);
        Assertions.assertEquals("hello world!",object);
    }
}

测试结果如下:

image-20210802113049113

数据库测试

当测试方法加上@Transaction注解时,测试方法中数据库的操作将会默认回滚,不污染数据库。

示例代码如下:

package com.example.demo;
​
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.dto.User;
import com.example.demo.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
​
​
/**
 * @description:
 * @time: 2021/8/4 16:15
 */
@SpringBootTest
public class DatabaseTest {
​
    @Autowired
    UserMapper userMapper;
​
    @Test
    @Transactional
    void databaseInsertTest(){
        User entity = new User();
        entity.setUserName("San Zhang");
        entity.setUserId(2L);
        userMapper.insert(entity);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        User user = userMapper.selectById(3);
        Assertions.assertEquals(null,user);
    }
}
​

执行结果如下:

image-20210804164356280

image-20210804164459220

可见数据库表中没有实际插入。