springboot系列文章之使用单元测试

springboot提供了 spirng-boot-starter-test 以供开发者使用单元测试,在引入 spring-boot-starter-test 依赖后:

org.springframework.bootspring-boot-starter-testtest复制代码

其中包含以下几个库:

Junit ——常用的单元测试库

Spring Test & Spring Boot Test ——对Spring应用的集成测试支持

AssertJ——一个断言库

Hamcrest—— 一个匹配对象的库

Mockito—— 一个Java模拟框架

JSONassert—— 一个针对JSON的断言库

JsonPath—— 用于JSON的XPath

下面我们将从Service层和Controller层的角度来简单介绍下单元测试

Service单元测试

在SpringBoot 2.0中,创建一个Service的单元测试,代码如下:

@RunWith(SpringRunner.class)@SpringBootTestpublicclassUserServiceImplTest{@AutowiredprivateUserService userService;@TestpublicvoidinsertUser(){        User user =newUser();        user.setUsername("li ning");        user.setPassword("123456");        userService.insertUser(user);    }}复制代码

上面的测试非常简单,主要需要注意两个注解: @RunWith 和 @SpringBootTest

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

@SpringBootTest 为 springApplication创建上下文并支持SpringBoot特性

使用 @SpringBootTest 的 webEnvironment 属性定义运行环境:

Mock(默认) : 加载WebApplicationContext 并提供模拟的web环境 Servlet环境,使用此批注时,不会启动嵌入式服务器

RANDOM_PORT : 加载WebServerApplicationContext 并提供真实的web环境,嵌入式服务器, 监听端口是随机的

DEFINED_PORT : 加载WebServerApplicationContext并提供真实的Web环境,嵌入式服务器启动并监听定义的端口(来自 application.properties或默认端口 8080)

NONE : 使用SpringApplication加载ApplicationContext 但不提供任何Web环境

Controller的单元测试

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

@RestControllerpublicclassUserController{@AutowiredprivateUserService userService;@PostMapping("/user")publicString userMapping(@RequestBodyUser user){        userService.insertUser(user);return"ok";    }}复制代码

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

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

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

@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvcpublicclassUserControllerTest{@AutowiredprivateMockMvc mockMvc;@TestpublicvoiduserMapping()throwsException{        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 有以下几个基本的方法:

performandExpectandDoandReturn

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

...importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.*;@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvcpublicclassUserControllerTest{@AutowiredprivateMockMvc mockMvc;@TestpublicvoiduserMapping()throwsException{        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)@SpringBootTestpublicclassUserControllerTest4{@AutowiredprivateWebApplicationContext web;privateMockMvc mockMvc;@BeforepublicvoidsetupMockMvc(){        mockMvc = MockMvcBuilders.webAppContextSetup(web).build();    }@TestpublicvoiduserMapping()throwsException{        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)publicclassUserControllerTest3{@AutowiredprivateTestRestTemplate testRestTemplate;@Testpublicvoid userMapping() throws Exception {        User user = new User();        user.setUsername("pj_pj");        user.setPassword("123456");        ResponseEntity 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 theendofeachtestmethodbydefault. However,asusingthis arrangementwitheither RANDOM_PORTorDEFINED_PORT implicitly provides arealservlet environment, theHTTPclientandserverruninseparate threadsand, thus,inseparate transactions.Anytransactioninitiatedontheserverdoesnotroll backinthiscase

解读一下,在单元测试中使用 @Transactional 注解,默认情况下在测试方法的末尾会回滚事务。然而有一些特殊情况需要注意,当我们使用 RANDOM_PORT 或 DEFINED_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 variableslike'%storage_engine%';复制代码

看具体的表user表用了什么引擎(engine后面的就表示当前表的存储引擎)

mysql> show createtableuser;复制代码

将user表修为InnoDB存储引擎

mysql> ALTER TABLEuserENGINE=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复制代码

如果你想了解更多,可以加群:855355016,群内有Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频免费获取

你可能感兴趣的:(springboot系列文章之使用单元测试)