SpringBoot2.x 整合单元测试 — QS12

本章节将梳理一下,我们日常开发中的单元测试场景。

目录(本文主要内容)

  • 需求背景
  • Service 层单元测试
    • DAO 测试(数据库操作,以Mybatis 为例)
    • 聚合服务的情况(比如,依赖某外部API)
  • Controller 层单元测试
  • Demo 下载:https://github.com/wangyushuai/springboot-quick-start
  • 单元测试结果:
    SpringBoot2.x 整合单元测试 — QS12_第1张图片

背景

单元测试还是很有必要的

试想,比如写一个一直不怎么变更的API ,那么可能Postman 跑一下,就完事儿了,但是这绝对是极少的情况 !!

工作几年的你我都知道, 出于产品需求的变更, 业务的迭代, 代码的质量的重构等等,这些原因常常需要我们改动代码, 特别是国内的互联网公司,业务迭代频繁,代码变动更加频繁, 如果单纯的依赖 “手点功能测试”, 那简直就是灾难。

如何解决以上的开发自测问题? 没错,就是单元测试 !

下面我们就介绍一下, 工作中常用的单元测试场景!

Service层单元测试

DAO层单元测试场景

由于代码中,已经有详细注释了,在此不具体展开,只说一下注意事项

  • @Transactional: 保证了单元测试结束后,会进行数据回滚,不会产生脏数据
  • @SpringBootTest : 将装载SpringBoot, 保证我的Bean 及配置正常加载

package com.example.springboot.service.impl;

import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;

import static org.junit.Assert.*;

/**
 * 读写数据库单元测试场景
 * 由于我们的TestTableService 直接调用了Mapper,故此处我们直接基于Service 进行单元测试
 * 这个场景需要保证数据正常连接(否则数据库连接池无法完成初始化)
 * @author [email protected]
 * @date 2019/8/5
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestTableServiceImplTest {

    @Autowired
    TestTableService testTableService;

    /**
     * 测试写入的参数
     */
    private TestTable testTable;

    /**
     * 每个测试方法执行前,将执行此方法
     */
    @Before
    public void setUp() {
        // 完成对测试参数的初始化
        testTable = new TestTable();
        testTable.setName("unit_test");
        testTable.setAge(18);
        testTable.setPhone("1234567890");
        testTable.setCreateTime(new Date());
    }

    /**
     * 对数据库的添加操作进行单元测试
     * 测试逻辑如下:
     * 调用Add方法,写入一条数据
     * 调用查询方法,判断是否写入成功
     * 注解@Transactional作用为: 单元测试方法体结束后,将进行数据回滚,不会产生脏数据(数据库写入单元测试的必备良选)
     */
    @Transactional
    @Test
    public void add() {
        // 写入,写入成功后,主键Id将对testTable 赋值
        testTableService.add(testTable);
        // 读取(此处应该基于selectOne正确情况下)
        TestTable writeResult = testTableService.selectOne(testTable.getId());
        Assert.assertNotNull("写入操作是否成功",writeResult);
        Assert.assertEquals("写入操作是否成功",testTable.getName(),writeResult.getName());
    }
}

存在外部API依赖单元测试场景

TestRequestApiService 类中写了 调用对外API 的逻辑,在聚合服务的类(Manager)中同时含有 DB操作和调用外部API的操作,如何单元测试?

这种场景,我们一般不关注 外部API 的正确性,我们一般只需要保证 外部API 正常,我们的逻辑正常就可以了。所以,一般采用Mock ,打桩的方法,对外部API 进行模拟,拿到正确的结果后,对我们自己写的逻辑,进行断言。

SpringBoot 单元测试依赖中 内置了 Mockito, 我们可以直接使用

划重点:

  • @Mock, @InjectMock (组合使用, 无需启动整个项目,速度较快)
  • @SpringBootTest, @MockBean (组合使用,由于启动了项目,加载了所有依赖,只会替换Mock的bean, 其他的bean 将是正常的bean)
  • given (Mockito 的静态方法) 和 willReturn

注意事项:

  • 由于我们这个类中,只有两个依赖,所以不需要加载整个SpringBoot项目,所以没有使用@SpringBootTest 注解
  • 所以在Mock 的使用,我使用了@Mock(生成一个Mock对象) 和 @InjectMock(注入Mock对象), 这和在有@SpringBoot注解时,@MockBean 作用时一致的。
  • 但是要注意,由于manager中,只有两个Dependency,两个我都有Mock, 如果有多个,需要多个都Mock ,不然初始化会失败。
package com.example.springboot.service.impl;

import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestRequestApiService;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.context.junit4.SpringRunner;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.given;

/**
 * 测试聚合服务的场景
 * @author [email protected]
 * @date 2019/8/5
 */
@RunWith(SpringRunner.class)
public class TestManagerServiceImplTest {
    /**
     * Mock TestTable Service 服务
     */
    @Mock
    TestTableService testTableService;

    /**
     * Mock TestRequestApiService
     */
    @Mock
    TestRequestApiService testRequestApiService;

    /**
     * 将Mock 注入我们要测试的服务
     * 注意: @InjectMocks 要使用非抽象方法
     */
    @InjectMocks
    TestManagerServiceImpl testManagerService;

    @Test
    public void fun() {
        TestTable obj = new TestTable();
        obj.setName("unit_test");
        given(testTableService.selectOne(anyLong())).willReturn(obj);
        given(testRequestApiService.getHelloDetail(anyString())).willReturn("Welcome unit_test");
        // 上面我们对Manager依赖的服务,进行了Mock, 在此种条件下,我们测试的方法应该返回 true
        // 故我们断言如下
        Assert.assertTrue("聚合服务fun方法正常情况",testManagerService.fun(1L));
        //TODO: 其他错误或异常情况,读者可以自行扩展,在此不展开,欢迎大家提问交流
    }
}

Controller 层单元测试

划重点

  • @AutoConfigureMockMvc
  • MockMvc mockMvc (以及他的Perform,header,andExcept等语法)
package com.example.springboot.controller;

import com.example.springboot.domain.TestTable;
import com.example.springboot.service.TestTableService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;

import javax.validation.constraints.NotNull;

import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * 控制器单元测试
 * @author [email protected]
 * @date 2019/8/18
 */
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class HelloControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    TestTableService testTableService;

    @Before
    public void setUp() throws Exception {
        // mock 数据库查询
        TestTable mockTestTable = new TestTable();
        mockTestTable.setName("unitTest");
        BDDMockito.given(testTableService.selectOne(anyLong())).willReturn(mockTestTable);
    }

    @Test
    public void helloDetail() throws Exception {

        String testRequestPath = "/api/v1/hello/1";
        mockMvc.perform(get(testRequestPath)
                .contentType(MediaType.APPLICATION_JSON)// 设置ContentType
                //.header()
                .content("{}") // 设置Body
                .accept(MediaType.TEXT_PLAIN)
        ).andExpect(status().isOk()) // 断言请求状态
                .andExpect( c -> assertNotNull(c.getResponse().toString())) // 自定义ResultMatcher语法
                .andExpect(content().string("unitTest"))// 直接对结果进行比较
        ;
    }


}

参考及推荐

本文直接介绍了单元测试场景,如何使用,下面的博客有介绍语法知识,比较全,推荐

https://blog.csdn.net/qq_35915384/article/details/80227297

你可能感兴趣的:(Java框架,SpringBoot2.x,TDD)