单元测试的正确姿势

Java
208
0
0
2024-05-11

什么是单元测试

  • 一个单元指的是应用程序中可测试的最小的一组源代码。
  • 源代码中包含明确的输入和输出的每一个方法被认为是一个可测试的单元。
  • 单元测试也就是在完成每个模块后都进行的测试。从确保每个模块没有问题,从而提高整体的程序质量。

单元测试的目的

  • 是将应用程序的所有源代码,隔离成最小的可测试的单元,保证每个单元的正确性。
  • 理想情况下,如果每个单元都能保证正确,就能保证应用程序整体相当程度的正确性。
  • 单元测试也是一种特殊类型的文档,相对于书面的文档,测试脚本本身往往就是对被测试代码的实际的使用代码,对于帮助开发人员理解被测试单元的使用是相当有帮助的。

适用范围

java后端研发人员

单元测试框架

推荐使用:Junit5 & Mockito

测试框架如Mockito或Powermock这里也不赘述。同理idea插件的对比,这里也不赘述。

版本依赖

<!-- junit-jupiter依赖 -->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.9.2</version>
    <scope>test</scope>
</dependency>

<!-- mockito-inline相关内联依赖,里面包含了mockito-core,因此无需额外引入mockito-core,
mockito-inline增强了对静态类测试的方法;替代powermock; 原因是:powermock目前并不支持junit5
 -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>5.0.0</version>
    <scope>test</scope>
</dependency>

Spring-boot 2.2版本及以上,默认采用了Junit5,如果引入spring-boot-starter-test版本为2.2+,会默认引入jupiter和mockito-core(注意mockito-core并不支持静态类测试)

生成单元测试代码

Idea插件

1、Squaretest(收费)

2、TestMe(免费)

3、JunitGenerator

......

这里使用的是:TestMe。其他插件都大同小异。

静态类测试

1、@BeforeEach注册静态类模拟实例

2、@AfterEach在测试完成后,关闭该实例

package org.example.utils;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

class TestUtilTest {

    private MockedStatic<TestUtil> testUtilStatic;

    /**
     * 注册一个静态类模拟实例
     */
    @BeforeEach
    public void setUp(){
        testUtilStatic = Mockito.mockStatic(TestUtil.class);
    }

    /**
     * 模拟的实例需要关闭
     */
    @AfterEach
    public void teardown(){
        if(testUtilStatic != null){
            testUtilStatic.close();
        }
    }

    @Test
    void testAdd() {
        // 模拟构造返回
        testUtilStatic.when(() -> TestUtil.add(Mockito.anyInt(), Mockito.anyInt())).thenReturn(3);
        // 断言验证结果
        Assertions.assertEquals(3, TestUtil.add(1, 2));
    }
}

//Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme

Controller层测试

1、@BeforeEach中获取mockMvc实例

2、@Mock 模拟外部实例类,如调用的service

3、通过mockMvc调用http method请求,断言返回的http_status是否符合预期

package org.example.controller;

import org.example.entity.TestEntity;
import org.example.service.TestService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Arrays;
import java.util.List;

import static org.mockito.Mockito.*;

@AutoConfigureMockMvc
class TestControllerTest {

    @Autowired
    protected MockMvc mockMvc;
    @Mock
    TestService testService;
    @InjectMocks
    TestController testController;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
        mockMvc = MockMvcBuilders.standaloneSetup(testController).build();
    }

    @Test
    void testQueryList() throws Exception {

        // 模拟数据库获取数据,并且期望返回值
        when(testService.queryList()).thenReturn(Arrays.asList(TestEntity.builder().name("嬴政").id(0).build()));

        MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/list")
                          // 接口参数
//                        .param("name", "张三")
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        Assertions.assertEquals(200, result.getResponse().getStatus());
    }

}

Service层测试

1、@BeforeEach中开启mock环境

2、@Mock 模拟外部实例类,如调用的dao

3、断言方法的返回值,是否符合预期

package org.example.service;

import org.example.dao.TestDao;
import org.example.entity.TestEntity;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.annotation.Rollback;

import java.util.Arrays;
import java.util.List;

import static org.mockito.Mockito.*;

/**
 * @author 彭耀煌
 */
class TestServiceTest {
    @Mock
    TestDao testDao;
    @InjectMocks
    TestService testService;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testQueryList() {
        when(testDao.queryList()).thenReturn(Arrays.<TestEntity>asList(new TestEntity(0, "name")));

        List<TestEntity> result = testService.queryList();
        Assertions.assertEquals(Arrays.<TestEntity>asList(new TestEntity(0, "name")), result);
    }

    @Test
    void getById(){
        TestEntity testEntity = new TestEntity(2, "武则天");
        when(testDao.queryList()).thenReturn(Arrays.<TestEntity>asList(testEntity));
        Assertions.assertSame(testEntity, testService.getById(2));
    }

    @Test
    void testUpdateById() {
        testService.updateById(anyInt());
    }
}

//Generated with love by TestMe :) Please report issues and submit feature requests at: http://weirddev.com/forum#!/testme

Mock

Mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个Mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。 Mock有很多的实现框架,例如Mockito、EasyMock、Jmockit、PowerMock、Spock等等,SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以了。