SpringBoot 集成 DBUnit 、database-rider与H2数据库进行单元测试

原创
2019/01/19 15:42
阅读数 3.9K

单元测试

什么是单元测试

参考维基百科: 单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单元测试的收益

  • 发现问题:单元测试可以在软件开发的早期就能发现问题。
  • 适应变更:单元测试允许程序员在未来重构代码,并且确保模块依然工作正确。
  • 简化集成:单元测试消除程序单元的不可靠,通过先测试程序部件再测试部件组装,使集成测试变得更加简单,更有信心。
  • 文档记录:单元测试提供了系统的一种文档记录。借助于查看单元测试提供的功能和单元测试中如何使用程序单元,开发人员可以直观的理解程序单元的基础 API。
  • 表达设计:在测试驱动开发的软件实践中,单元测试可以取代正式的设计。每一个单元测试案例均可以视为一项类、方法和待观察行为等设计元素。

单元测试的局限

测试不可能发现所有的程序错误,单元测试也不例外。单元测试只测试程序单元自身的功能。因此,它不能发现集成错误、性能问题、或者其他系统级别的问题。

Junit单元测试

常用注解

  • @Test:注解的 public void 方法将会被当做测试用例,JUnit 每次都会创建一个新的测试实例,然后调用 @Test 注解方法,任何异常的抛出都会认为测试失败。
  • @Before:执行测试方法前需要统一预处理的一些逻辑,该方法在每个 @Test 注解方法被执行前执行
  • @After:该方法在每个 @Test 注解方法执行后被执行。
  • @BeforeClass:该方法会在所有测试方法被执行前执行一次,并且只执行一次。
  • @AfterClass:该方法会在所有测试方法被执行后执行一次,并且只执行一次。
  • @Ignore:对包含测试类的类或 @Test 注解方法使用 @Ignore 注解将使被注解的类或方法不会被当做测试执行;JUnit 执行结果中会报告被忽略的测试数。
  • @FixMethodOrder:Junit 4.11 里增加了指定测试方法执行顺序的特性,测试类的执行顺序可通过对测试类添加注解 @FixMethodOrder(value)来指定。
    • MethodSorters.DEFAULT:(默认)默认顺序由方法名 hashcode 值来决定,如果 hash 值大小一致,则按名字的字典顺序确定。
    • MethodSorters.NAME_ASCENDING:(推荐)按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致
    • MethodSorters.JVM:按 JVM 返回的方法名的顺序执行,此种方式下测试方法的执行顺序是不可预测的,即每次运行的顺序可能都不一样

Junit实例

实例代码如下:

package com.ejyi.demo.springboot.server.web.unittest;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.util.Date;

/**
 * @author tree
 * @version 1.0
 * @description Junit单元测试示例
 * @create 2019-01-17 8:26 PM
 */
@FixMethodOrder(value = MethodSorters.NAME_ASCENDING)
public class JUnitTest {

    private static int num = 0;

    @Test
    public void test1(){
        System.out.println("test1 run...");
        System.out.println("test1 num = " + num);
        num = 1;
        System.out.println("test1 num = " + num);
    }

    @Test
    public void test2(){
        System.out.println("test2 run...");
        System.out.println("test2 num = " + num);
        num = 2;
        System.out.println("test2 num = " + num);
    }

    @Test
    public void test3(){
        System.out.println("test3 run...");
        System.out.println("test3 num = " + num);
        num = 3;
        System.out.println("test3 num = " + num);
    }

    @Test
    public void test4(){
        System.out.println("test4 run...");
        System.out.println("test4 num = " + num);
        num = 4;
        System.out.println("test4 num = " + num);

        Long time = System.currentTimeMillis();
        Date date1 = new Date(time);
        Date date2 = new Date(time);

        Assert.assertEquals(date1, date2);
        Assert.assertTrue(date1 == date2);
    }

    @Before
    public void beforeEveryTest(){
        System.out.println("=== beforeEveryTest ===");
    }

    @After
    public void afterEveryTest(){
        System.out.println("=== afterEveryTest ===");
    }

    // must be static
    @BeforeClass
    public static void beforeClassTest(){
        System.out.println("===beforeClassTest===");
    }

    // must be static
    @AfterClass
    public static void afterClassTest(){
        System.out.println("===afterClassTest===");
    }


}

通过Assert可以对返回值和预期值进行对比。输出如下:

===beforeClassTest===
=== beforeEveryTest ===
test1 run...
test1 num = 0
test1 num = 1
=== afterEveryTest ===
=== beforeEveryTest ===
test2 run...
test2 num = 1
test2 num = 2
=== afterEveryTest ===
=== beforeEveryTest ===
test3 run...
test3 num = 2
test3 num = 3
=== afterEveryTest ===
=== beforeEveryTest ===
test4 run...
test4 num = 3
test4 num = 4
=== afterEveryTest ===

java.lang.AssertionError
	at org.junit.Assert.fail(Assert.java:86)
	at org.junit.Assert.assertTrue(Assert.java:41)
	at org.junit.Assert.assertTrue(Assert.java:52)
	...

===afterClassTest===

可以看到对于 “Assert.assertTrue(date1 == date2);”,提示失败。

SpringBoot的单元测试

Spring Boot 单元测试可以参考官网文档: https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html

下面针对我认为比较常用用法进行说明。

常用注解

  • @RunWith:就是一个运行器,告诉 junit 的框架应该是使用哪个 testRunner。
    • @RunWith(JUnit4.class) 就是指用 JUnit4 来运行
    • @RunWith(SpringRunner.class), 让测试运行于 Spring 测试环境
  • @SpringBootTest:是 SpringBoot 自 1.4.0 版本开始引入的一个用于测试的注解。webEnvironment属性指定启动策略。一般用 RANDOM_PORT 随机端口
  • @ActiveProfiles:指定用到的配置文件名。
  • @AutoConfigureMockMvc:自动mock MockMvc对象;
  • @SpyBean:用于mock某一个对象;

实例

定义基类

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("local")
@AutoConfigureMockMvc
public class BaseTest {
}

实际使用中 DemoControllerTestByMockMvc 类通过mockMvc对象进行接口单元测试,实际代码如下:

public class DemoControllerTestByMockMvc  extends BaseTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testQuery() throws Exception {        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/v1/demo/2").contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
                .andReturn().getResponse().getContentAsString();
    }

    @Test
    public void testAdd() throws Exception {
        Long time = System.currentTimeMillis();
        DemoModel demoModel = new DemoModel(null, time.toString(), 34.1D, (byte) 1, new Date(), new Date());

        this.mockMvc.perform(MockMvcRequestBuilders.post("/demo/v1/demo")
                .contentType(MediaType.APPLICATION_JSON_UTF8).content(JsonUtils.toJson(demoModel)))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
                .andReturn().getResponse().getContentAsString();
    }
}

DemoControllerTestByMockService类,通过 @SpyBean 注解和 BDDMockito 搭配进行方法的mock,而且可以仅针对特定参数进行mock,不会影响其他的访问行为。代码如下:

public class DemoControllerTestByMockService extends BaseTest {


    @SpyBean
    private DemoService demoService;

    @Autowired
    private MockMvc mockMvc;


    @Test
    public void test01Query() throws Exception {

        DemoModel demoModel = new DemoModel();
        demoModel.setId(100L);
        demoModel.setCode("100");
        demoModel.setUpdateTime(new Date());
        demoModel.setStatus((byte)1);
        demoModel.setScore(1.1D);
        demoModel.setCreateTime(new Date());
        BDDMockito.given(this.demoService.queryById(demoModel.getId())).willReturn(CallResult.makeCallResult(true, ResultEnum.SUCCESS, demoModel, null));

        CallResult<DemoModel> demoModelCallResult = this.demoService.queryById(100L);

        System.out.println(JsonUtils.toJson(demoModelCallResult));

        Assert.assertTrue(demoModelCallResult.isSuccess());
        Assert.assertTrue(demoModelCallResult.getBusinessResult().getCode().equals("100"));

        demoModelCallResult = this.demoService.queryById(1L);

        System.out.println(JsonUtils.toJson(demoModelCallResult));
        Assert.assertTrue(demoModelCallResult.isSuccess());
    }


    @Test
    public void test02Query() throws Exception {

        DemoModel demoModel = new DemoModel();
        demoModel.setId(100L);
        demoModel.setCode("100");
        demoModel.setUpdateTime(new Date());
        demoModel.setStatus((byte)1);
        demoModel.setScore(1.1D);
        demoModel.setCreateTime(new Date());
        BDDMockito.given(this.demoService.queryById(demoModel.getId())).willReturn(CallResult.makeCallResult(true, ResultEnum.SUCCESS, demoModel, null));
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/v1/demo/100").contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.code").value("100"))
                .andReturn().getResponse().getContentAsString();
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/v1/demo/1").contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.id").value(1))
                .andReturn().getResponse().getContentAsString();
    }

}

还可以直接基于数据库操作进行测试, 代码如下:

public class ActiveInfoMapper01Test extends BaseTest {

    @Autowired
    private ActiveInfoMapper activeInfoMapper;

    @Test
    public void insert() throws Exception {
        int count = 0;
        Long time = System.currentTimeMillis();
        ActiveInfoPO activeInfoPO = new ActiveInfoPO(time, time.toString(), 10, 1.1D, 1, 1, new Date(), new Date());
        count = activeInfoMapper.insert(activeInfoPO);
        assertEquals(1, count);
        System.out.printf("==========================================ActiveInfoMapper01Test.insert%s", JsonUtils.toJson(activeInfoPO));
    }


    @Test
    public void queryById() throws Exception {
        ActiveInfoPO activeInfoPO = activeInfoMapper.selectById(2L);
        Assertions.assertThat(activeInfoPO.getId()).isEqualTo(2L);
        System.out.printf("==========================================ActiveInfoMapper01Test.queryById%s", JsonUtils.toJson(activeInfoPO));
    }
}

基于H2数据库进行单元测试

有时候跑集成测试的时候,用例较多会相互影响,因此可以考虑基于H2的内存数据库来作为测试用库。新建测试配置文件application-unittest.yml.

spring:
  datasource:
    hikari:
      jdbc-url: jdbc:h2:mem:springboot_demo;MODE=MYSQL;DB_CLOSE_DELAY=-1  
      username: root
      password:
      driver-class-name: org.h2.Driver
      maximum-pool-size: 15
      pool-name: unit-test-db
    name: demo
    filters: config,log4j,stat
    platform: h2
  h2:
    console:
      enabled: true

同时还用了flyway模块,对数据库基础数据进行创建,database-rider模块进行测试环境数据创建,非常实用,github地址:https://github.com/database-rider/database-rider 代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("unittest")
@AutoConfigureMockMvc
@DBUnit()
public class BaseH2MockTest {

    private static String DB_URL = "jdbc:h2:mem:springboot_demo;MODE=MYSQL;DB_CLOSE_DELAY=-1";
    private static String DB_USER = "root";
    private static String DB_PASSWORD = "";

    private static Flyway flyway;

    @Rule
    public DBUnitRule dbUnitRule = DBUnitRule.
            instance(() -> flyway.getDataSource().getConnection());

    @BeforeClass
    public static void initMigration() throws SQLException {

        flyway = Flyway.configure().dataSource(DB_URL, DB_USER, DB_PASSWORD)
                .locations("filesystem:src/test/resources/db/migration").load();
        flyway.migrate();
    }

    @AfterClass
    public static void cleanMigration() throws SQLException {
//        flyway.clean();
//        if (!connection.isClosed()) {
//            connection.close();
//        }
    }
}

DemoControllerTestByH2Mock类进行测试,其中@Sql是spring自带的数据初始化注解,@DataSet是database-rider提供的注解,@Sql对于执行多次可能会相互影响,可以把test03Query的注释去掉试着跑一下。

public class DemoControllerTestByH2Mock extends BaseH2MockTest {

    private final static Logger logger = LoggerFactory.getLogger(DemoControllerTestByH2Mock.class);

    @Autowired
    private MockMvc mockMvc;

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void test01Query() throws Exception{

        ResponseEntity<String> responseEntity = restTemplate.getForEntity("/demo/v1/demo/1", String.class);

        System.out.println("-----------------------------------------DemoControllerTestByH2Mock.testQuery::"+responseEntity);

        Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(responseEntity.getBody()).contains("\"code\":200").contains("\"id\":1");
    }

    @Test
    @Sql(value = {"/db/data/init_active_info.sql"})
    public void test02Query() throws Exception{

        ResponseEntity<String> responseEntity = restTemplate.getForEntity("/demo/v1/demo/3", String.class);

        System.out.println("-----------------------------------------DemoControllerTestByH2Mock.testQuery1::"+responseEntity);

        Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(responseEntity.getBody()).contains("\"code\":200").contains("\"id\":3");
    }

//    @Test
//    @Sql(value = {"/db/data/init_active_info.sql"})
//    public void test03Query() throws Exception{
//        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/v1/demo/4").contentType(MediaType.APPLICATION_JSON_UTF8))
//                .andDo(MockMvcResultHandlers.print())
//                .andExpect(MockMvcResultMatchers.status().isOk())
//                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
//                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
//                .andExpect(MockMvcResultMatchers.jsonPath("$.data.id").value(4))
//                .andReturn().getResponse().getContentAsString();
//    }



    @Test
    @DataSet(value = {"db/data/active_info.yml"})
    public void test04Query() throws Exception{
        ResponseEntity<String> responseEntity = restTemplate.getForEntity("/demo/v1/demo/8", String.class);

        System.out.println("-----------------------------------------DemoControllerTestByH2Mock.testQuery2::"+responseEntity);

        Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        Assertions.assertThat(responseEntity.getBody()).contains("\"code\":200").contains("\"id\":8");

    }

    @Test
    @DataSet(value = {"db/data/active_info.yml"})
    public void test05Query() throws Exception{
        this.mockMvc.perform(MockMvcRequestBuilders.get("/demo/v1/demo/9").contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
                .andExpect(MockMvcResultMatchers.jsonPath("$.data").exists())
                .andExpect(MockMvcResultMatchers.jsonPath("$.data.id").value(9))
                .andReturn().getResponse().getContentAsString();

    }
}

以上介绍的几种测试方式,应该可以覆盖我们绝大部分的测试场景了,对于增强代码的健壮性和可维护性,非常建议大家写测试用例。

代码见如下地址:

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部