测试-SpringBoot单元测试

1. 背景

目前应该比较多的公司对单元测试这块应该涉及的都比较少。开发层面一般比较难养成从一开始就写单元测试去测试接口。可能有一下一部分原因:

  • 时间少(开发都来不及,就别提单元测试了)
  • YAPI、Google Talend这类接口测试工具
  • 单元测试写起来还比较麻烦,各种依赖中间件、第三方接口都需要写mock数据

2. 概论

(1)@SpringBootTest

@RunWith(SpringRunner.class)
//@SpringBootTest(classes = BaseApplication.class)
//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class BaseTest {

}
  • classes属性可以直接指定自定义启动类
@SpringBootApplication(scanBasePackages = {
        "priv.whh.std.boot.junit"
})
public class BaseApplication {
    public static void main(String[] args) {
        SpringApplication.run(BaseApplication.class, args);
    }
}
  • webEnvironment
// 加载WebApplicationContext并提供Mock Servlet环境,嵌入的Servlet容器不会被启动
SpringBootTest.WebEnvironment.MOCK
// 加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个默认的端口上监听(application.properties配置端口或者默认端口8080)
SpringBootTest.WebEnvironment.DEFINED_PORT
// 加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个随机端口上监听
SpringBootTest.WebEnvironment.RANDOM_PORT
// 使用SpringApplication加载一个ApplicationContext,但是不提供任何的servlet环境
SpringBootTest.WebEnvironment.NONE

Q:模拟的Servlet环境和真实的Servlet有什么区别?

Q:不提供Servlet环境能干什么,不能干什么(SpringBootTest.WebEnvironment.NONE)?

(2)@MockBean

在你的ApplicationContext里为一个bean定义一个Mockito mock。直白的说,就是注入一个demo的bean来代替真实的bean注入。当目标测试方法执行到Mockio模拟的方法时,可以按照预定内容返回

@MockBean
private OtherService otherService;
@Test
public void updateTest() {
    // Mockito.any()这类可以模拟入参 ---> otherService.check(Mockito.any())
    Mockito.when(otherService.check()).thenReturn(Boolean.TRUE);
    User user = userService.update();
    Assert.assertNull(user);
}

(3)@SpyBean

定制化Mock某些方法。使用@SpyBean除了被打过桩的函数,其它的函数都将真实返回。有些例如h2数据库不兼容语法的方法,我们需要跳过,但是其他方法我们需要真实调用。

@SpyBean
@Autowired(required = false)
private UserService userService;

@Test
public void saveTest() {
    // 该service中check方法打桩
    Mockito.when(userService.check()).thenReturn(Boolean.FALSE);
    User user = userService.save();
    Assert.assertNotNull(user);
}

(4)@WebMvcTest + @AutoConfigureMockMvc

前面的一些注解对于基本的单元测试已经够用,其实Controller如果不去检验接口参数的合法性(如validation这类的校验注解),也是可以不用@WebMvcTest 这类注解的。

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
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.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import priv.whh.std.boot.junit.model.User;

import javax.annotation.Resource;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = {UserController.class})
//@SpringBootTest(classes = {BaseApplication.class})
// 该注解表示 MockMvc由spring容器构建
@AutoConfigureMockMvc
public class UserControllerTest {
    @Resource
    private MockMvc mockMvc;

    @Test
    public void getByIdTest() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/user/{id}/get?name={name}", 1, "test"))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

    @Test
    public void saveTest() throws Exception {
        User user = new User();
        user.setName("test");
        user.setAge(10);
        mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/user/{id}/post?name={name}", 1, "test",
                user).contentType(MediaType.APPLICATION_JSON).content(JSON.toJSONString(user)))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

(5)H2数据库

单元测试不能连接真实mysql数据库,否则就没什么意义。但是内存数据库h2与mysql语法上存在一定差异,不能完全兼容。虽然连接方式上可以设置Mode模式为MySQL, 但目前看来只能兼容一部分语法,还是不能完全兼容,例如data_fornat函数等。

maven依赖:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>

application.yaml: 

spring:
  # 数据库配置
  datasource:
    url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1
    username: root
    password: root
    driver-class-name: org.h2.Driver
    # 数据库初始化表 DDL
    schema:
      - classpath*:db/schema/*.sql
    # 数据库初始化数据 DML
#    data:
#      - classpath*:db/data/*.sql
  #mybatis-plus配置
mybatis-plus:
  type-aliases-package: priv.whh.std.boot.junit.model
  configuration:
    map-underscore-to-camel-case: true

schema.sql:

CREATE TABLE `t_user`(
    `id`           bigint(20)  NOT NULL AUTO_INCREMENT COMMENT '主键id',
    `name`         varchar(32) NOT NULL COMMENT '创建人',
    `age`          int(8)      NOT NULL COMMENT '修改人',
    `gmt_create`   timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `gmt_modified` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`),
    KEY `name` (`name`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

UserDaoTest:

import org.junit.Assert;
import org.junit.Test;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import priv.whh.std.boot.junit.BaseTest;
import priv.whh.std.boot.junit.model.User;

import java.util.Date;


@MapperScan("priv.whh.std.boot.junit.dao")
public class UserDaoTest extends BaseTest {
    @Autowired
    private UserDao userDao;

    @Test
    public void insertTest() {
        User user = new User();
        user.setAge(1);
        user.setName("test");
        int num = userDao.insert(user);
        Assert.assertTrue(num > 0);
    }

    @Test
    public void testSqlTest() {
        Boolean num = userDao.testSql(new Date(), new Date());
        Assert.assertTrue(num);
    }
}

3. 其他

Q:h2内存数据库与Mysql不完全兼容,没有替代品?

Q:@DataJdbcTest、@DataRedisTest等等,对单元测试有什么帮助,目前看来不能替代数据库及Redis?

 

参考资料:

SpringBootTest 测试工具

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页