springboot test

Spring Boot提供了一些注解和工具去帮助开发者测试他们的应用。在项目中使用Spring Boot Test支持,只需要在pom.xml引入如下配置即可:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

1. spring-boot-start-test

SpringBoot提供了spring-boot-start-test启动器,该启动器提供了常见的单元测试库:

  • JUnit: 一个Java语言的单元测试框架。
  • Spring Test & Spring Boot Test:为Spring Boot应用提供集成测试和工具支持。
  • AssertJ:支持流式断言的Java测试框架。
  • Hamcrest:一个匹配器库。
  • Mockito:一个java mock框架。
  • JSONassert:一个针对JSON的断言库。
  • JsonPath:JSON XPath库。

常用注解

  • @RunWith: 该注解标签是Junit提供的,用来说明此测试类的运行者,这里用了SpringRunner,它实际上继承了 SpringJUnit4ClassRunner类,而 SpringJUnit4ClassRunner这个类是一个针对Junit 运行环境的自定义扩展,用来标准化在Springboot环境下Junit4.x的测试用例
  • @SpringBootTest 为 springApplication创建上下文并支持SpringBoot特性

使用@SpringBootTestwebEnvironment属性定义运行环境:

  • Mock(默认): 加载WebApplicationContext 并提供模拟的web环境 Servlet环境,使用此批注时,不会启动嵌入式服务器
  • RANDOM_PORT: 加载WebServerApplicationContext 并提供真实的web环境,嵌入式服务器,监听端口是随机的
  • DEFINED_PORT: 加载WebServerApplicationContext并提供真实的Web环境,嵌入式服务器启动并监听定义的端口(来自 application.properties或默认端口 8080)
  • NONE: 使用SpringApplication加载ApplicationContext 但不提供任何Web环境

基本示例

以下为基于Junit4的单元测试基本示例。引入spring-boot-start-test之后默认采用Junit4。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {
    @Autowired
    private UserService userService;
    @Test
    public void insertUser() {
        User user = new User();
        user.setUsername("li ning");
        user.setPassword("123456");
        userService.insertUser(user);
    }
}

Controller的单元测试

首先创建一个Controller,代码如下:

@RestController
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/user")
    public String userMapping(@RequestBody User user){
        userService.insertUser(user);
        return "ok";
    }
}

然后创建Controller的单元测试,一般有两种创建方法。

第一种使用模拟环境进行测试

默认情况下,@SpringBootTest 不会启动服务器,如果需针对此模拟环境测试Web端点,可以如下配置 MockMvc:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(MockMvcRequestBuilders.request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("ok"));
    }
}

这里有一个 @AutoConfigureMockMvc注解,该注解表示启动测试的时候自动注入 MockMvc,而这个MockMvc有以下几个基本的方法:

  • perform : 执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理。
  • andExpect: 添加RequsetMatcher验证规则,验证控制器执行完成后结果是否正确
  • andDo: 添加ResultHandler结果处理器,比如调试时打印结果到控制台
  • andReturn: 最后返回相应的MvcResult,然后进行自定义验证/进行下一步的异步处理

这里有一个小技巧,一般来说对于一个controller中往往有不止一个Request请求需要测试,敲打MockMvcRequestBuilders与MockMvcResultMatchers会显得比较繁琐,有一个简便的方法就是将这两个类的方法使用 import static静态导入,然后就可以直接使用两个类的静态方法了。然后代码就变成如下所示:

...
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_mike\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }
}

另外,如果是只想关注Web层而不是启动完整的ApplicationContext,可以考虑使用 @WebMvcTest 注解,该注解不能与@SpringBootTest搭配使用,而且它只关注Web层面,至于涉及到数据层的时候,需要引入相关依赖,关于这个注解更多的介绍请参阅官方文档: docs.spring.io/spring-boot…

使用MockMvcBuilder构建MockMvc对象

除了上面用 @AutoConfigureMockMvc 注解直接自动注入 MockMvc的方式,我们还可以利用MockMvcBuilder来构建MockMvc对象,示例代码如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserControllerTest4 {
    @Autowired
    private WebApplicationContext web;
    private MockMvc mockMvc;

    @Before
    public void setupMockMvc() {
        mockMvc = MockMvcBuilders.webAppContextSetup(web).build();
    }
    @Test
    public void userMapping() throws Exception {
        String content = "{\"username\":\"pj_m\",\"password\":\"123456\"}";
        mockMvc.perform(request(HttpMethod.POST, "/user")
                        .contentType("application/json").content(content))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }
}

第二种使用真实Web环境进行测试

在@SpringBootTest注解中设置属性 webEnvironment = WebEnvironment.RANDOM_PORT,每次运行的时候会随机选择一个可用端口。我们也可以还使用 @LoalServerPort注解用于本地端口号。下面是测试代码:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest3 {
    @Autowired
    private TestRestTemplate testRestTemplate;
    @Test
    public void userMapping() throws Exception {
        User user = new User();
        user.setUsername("pj_pj");
        user.setPassword("123456");
        ResponseEntity<String> responseEntity = testRestTemplate.postForEntity("/user", user, String.class);
        System.out.println("Result: "+responseEntity.getBody());
        System.out.println("状态码: "+responseEntity.getStatusCodeValue());
    }
}

上面的代码中有一个关键的类——TestRestTemplate, TestRestTemplate是Spring的RestTemplate的一种替代品,可用于集成测试,更RestTemplate的使用功能方法类似,一般用于真实web环境测试中,关于该类更加详细的用法参考官方文档: docs.spring.io/spring-boot…

单元测试回滚

单元测试的时候,如果不想造成垃圾数据,可以开启事务功能,在方法或类头部添加 @Transactional注解即可,在官方文档中对此也有说明:

If your test is @Transactional, it rolls back the transaction at the end of each test method by default. However, as using this arrangement with either RANDOM_PORT or DEFINED_PORT implicitly provides a real servlet environment, the HTTP client and server run in separate threads and, thus, in separate transactions. Any transaction initiated on the server does not roll back in this case

解读一下,在单元测试中使用 @Transactional注解,默认情况下在测试方法的末尾会回滚事务。然而有一些特殊情况需要注意,当我们使用 RANDOM_PORTDEFINED_PORT这种安排隐式提供了一个真正的Servlet环境,所以HTTP客户端和服务器将在不同的线程中运行,从而分离事务,这种情况下,在服务器上启动的任何事务都不会回滚。

当然如果你想关闭回滚,只要加上 @Rollback(false)注解即可,@Rollback表示事务执行完回滚,支持传入一个value,默认true即回滚,false不回滚。

还有一种情况需要注意,就是如果你使用的数据库是MySQL,有时候会发现加了注解 @Transactionl也不会回滚,那么你就要查看一下你的默认引擎是不是InnoDB,如果不是就要改成 InnoDB。

MyISAM 与 InnoDB是mysql目前比较常用的两个数据库引擎,MyISAM与InnoDB的主要的不同点在于性能和事务控制上,这里简单介绍下两者的区别与转换方法:

  • MyISAM: MyISAM是MySQL5.5之前版本默认的数据库存储引擎,MyISAM提供高速存储和检索,以及全文搜索能力,适合数据仓库等查询频繁的应用,但不支持事务和外键,不能在表损坏后恢复数据
  • InnoDB: InnoDB是MySQL5.5版本的默认数据库存储引擎,InnoDB具有提交,回滚和崩溃恢复能力的事务安全,支持事务和外键,比起MyISAM,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。

如果你的数据表是MyISAM引擎,由于它不支持事务,在单元测试中添加事务注解,测试方法也是不会回滚的。

修改默认引擎

  • 查看MySQL当前默认的存储引擎
mysql> show variables like '%storage_engine%';
复制代码
  • 看具体的表user表用了什么引擎(engine后面的就表示当前表的存储引擎)
mysql> show create table user;
复制代码
  • 将user表修为InnoDB存储引擎
mysql> ALTER TABLE user ENGINE=INNODB;

复制代码

注意

这里还有一点需要注意的地方,当我们使用Spring Data JPA时,如果没有指定MySQL建表时的存储引擎,默认情况下会使用MySQL的MyISAM,这也是一个坑点,这种情况下,你在单元测试使用@Transactional注解,回滚不会起作用。

解决方法是将 hibernate.dialect属性配置成hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect,指定MySQL建表的时候使用 InnoDB引擎,示例配置文件如下:

spring:
  jpa:
    # 数据库类型
    database: mysql
    # 输出日志
    show-sql: true
    properties:
      hibernate:
        # JPA配置
        hbm2ddl.auto: update
        # mysql存储类型配置
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

JUnit5单元测试

Unit 5跟以前的JUnit版本不一样,它由几大不同的模块组成,这些模块分别来自三个不同的子项目。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

核心注解

  • @BeforeAll 类似于JUnit 4的@BeforeAll,表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者
  • @TestFactory注解的方法之前执行,必须为static。
  • @BeforeEach 类似于JUnit 4的@Before,表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前执行。
  • @Test 表示该方法是一个测试方法。
  • @DisplayName 为测试类或测试方法声明一个自定义的显示名称。
  • @AfterEach 类似于JUnit 4的@After,表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行。
  • @AfterAll 类似于JUnit 4的@AfterClass,表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行,必须为static。
  • @Disable 用于禁用一个测试类或测试方法,类似于JUnit 4的@Ignore。
  • @ExtendWith 用于注册自定义扩展。

添加依赖

spring-boot-starter-test默认使用junit4,需要手动添加junit5依赖。可通过两种方式来添加:

<!-- Junit 5 -->
<dependency>
   <groupId>org.junit.jupiter</groupId>
   <artifactId>junit-jupiter</artifactId>
   <version>5.5.2</version>
   <scope>test</scope>
</dependency>

或通过如下方式:

<!-- Junit 5 -->
<dependency>
	<groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter-api</artifactId>
	<scope>test</scope>
	</dependency>
	<dependency>
	<groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter-params</artifactId>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.junit.jupiter</groupId>
	<artifactId>junit-jupiter-engine</artifactId>
	<scope>test</scope>
</dependency>

如果必要则可将spring-boot-starter-test中的junit-vintage-engine排除。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

代码示例

package com.secbro2.learn.service;

import com.secbro2.learn.model.Order;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
//@ExtendWith(SpringExtension.class)
@SpringBootTest
public class OrderServiceTest {

	@Resource
	private OrderService orderService;

	@Test
	public void testInsert() {
		Order order = new Order();
		order.setOrderNo("A001");
		order.setUserId(100);
		orderService.insert(order);
	}

} 

其中需要注意的是如果是spring boot 2.1.x之后版本只使用@SpringBootTest即可,如果是之前版本,还需要添加@ExtendWith(SpringExtension.class)注解,否则无法生效。

参考链接:https://juejin.im/post/5b95dbe46fb9a05cd7772503
相关链接:《SPRINGBOOT JUNIT5 单元测试

springboot 单元测试详解插图


springboot 单元测试详解插图1

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2019/11/13/springboot-junit-all/