单元测试

原博文地址:https://my.oschina.net/tree/blog/3003601

单元测试
什么是单元测试
参考维基百科: 单元测试(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(" . c o d e " ) . v a l u e ( 200 ) ) / / . a n d E x p e c t ( M o c k M v c R e s u l t M a t c h e r s . j s o n P a t h ( " .code").value(200)) // .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();

}

}

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

代码见如下地址:

https://github.com/treeyh/java-demo/tree/master/spring-boot/spring-boot-server
https://gitee.com/treeyh/java-demo/tree/master/spring-boot/spring-boot-server

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值