Spring Boot 应用的测试
《Spring Boot 实战开发》(陈光剑)
—— 基于 Gradle + Kotlin的企业级应用开发最佳实践
本书写到这里,Spring Boot 2.0.0.RC1版本已经于2018.1.31 发布。这是本书最后一章,本章介绍 Spring Boot 应用的测试(质量保障)相关的内容。我们在项目开发中使用分层架构,在测试中也进行分层测试。
1.1 准备工作
本节先来创建一个基于Spring MVC、 Spring Data JPA的 Spring Boot, 完成Dao 层、 Service 层、Controller 层代码的编写,为后面的测试代码的编写做准备。
使用http://start.spring.io/ 创建项目、导入此 Gradle 项目到 IDEA 中。配置 Kotlin Compiler 版本与Target JVM 版本。最后等待项目构建完毕。我们将得到一个初始Spring Boot 工程。详细的代码参考本章给出的示例工程源码。
下面我们来详细讲解怎样针对 Spring Boot 项目进行分层测试。
1.2 分层测试
我们在开发阶段过程中,单元测试通常是必要的。Spring Boot 提供的spring-boot-test 模块基于 spring-test 模块和junit 框架,封装集成了功能强大的结果匹配校验器assertj 、hamcrest Matcher、 Web 请求 Mock 对象、 httpclient、JsonPath (测试 JSON 数据)、mockito、selenium等。
测试代码通常放在 src/test 目录下,包目录规范是跟 src/main 目录保持一致。测试代码目录结构设计如下
图15-1 测试代码目录结构
测试代码的分层逻辑与项目源代码中的 dao层、service 层、controller 层各自对应。
下面我们来开发具体的测试类。
1.2.1 Dao 层测试
在包com.easy.springboot.demo_testing_and_deploy.dao下面添加UserDaoTest.kt测试类,代码如下
@RunWith(SpringRunner::class)
@SpringBootTest
class UserDaoTest {
@Autowired lateinit var userDao: UserDao
@Test
fun testFindAll() {
Assert.assertTrue(userDao.findAll().size == 2)
}
}
其中,需要测试类上需要添加@RunWith(SpringRunner.class) 和 @SpringBootTest 注解。这里的 @RunWith这里就不多做解释了,在 JUnit中这个是最常用的注解。
@SpringBootTest这个注解是SpringBoot项目测试的核心注解,标识该测试类以SpringBoot方式运行,该注解的定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
public @interface SpringBootTest{}
在上面的 @SpringBootTest 注解源码中最重要的是 @BootstrapWith,该注解配置了测试类的启动核心类SpringBootTestContextBootstrapper。
在UserDaoTest测试类中可以直接使用@Autowired来装配UserDao这个 Bean。而且,@SpringBootTest 注解会自动帮我们完成启动一个 Spring 容器 ApplicationContext,然后连接数据库,执行一套完整的业务逻辑。
1.2.2 Service 层测试
Service 层的代码测试类跟 Dao 层类似,例如UserServiceTest.kt 测试代码如下
@RunWith(SpringRunner::class)
@SpringBootTest
class UserServiceTest {
// 直接使用@Autowired注解注入 Service 对象
@Autowired lateinit var userService: UserService
@Test
fun testFindAll() {
Assert.assertTrue(userService.findAll().size == 2)
}
}
1.2.3 使用 Mockito 测试 Service 层代码
上面的测试代码是连接真实的数据库来执行真实的 Dao 层数据库查询逻辑。
而在实际开发的场景中,我们有时候需要独立于数据库进行 Service 层逻辑的开发。这个时候就可以直接把数据库Dao层代码Mock 掉。例如在UserService中有一个 getOne()方法,具体的实现代码是
interface UserService {
...
fun getOne(id:Long):User?
}
@Service
class UserServiceImpl : UserService {
@Autowired lateinit var userDao: UserDao
...
override fun getOne(id: Long): User? {
return userDao.getOne(id)
}
}
下面,我们就使用 Mockito 来把 UserDao 层代码 Mock 掉。Mockito 主要用于 service 层的 mock 测试。mock 的对象一般是对 DAO 层的依赖; 另外就是别人的Service实现类。
新建测试类MockUserServiceTest.kt 代码如下:
@RunWith(MockitoJUnitRunner::class)
class MockUserServiceTest {
@Mock
lateinit var mockUserDao: UserDao // mock 一个DAO层的接口
@InjectMocks
lateinit var userService: UserServiceImpl// Mock一个 Service 的实现类,用 @InjectMocks。注意这里是实现类 UserServiceImpl
@Before
fun setUp() {
// initMocks 必须,否则 @Mock 注解无效
MockitoAnnotations.initMocks(this)
}
@Test
fun testGetOne() {
val mockUser = User()
mockUser.id = 101
mockUser.username = "mockUser"
mockUser.password = "123456"
val roles = mutableSetOf<Role>()
val r1 = Role()
r1.role = "ROLE_USER"
val r2 = Role()
r1.role = "ROLE_ADMIN"
roles.add(r1)
roles.add(r2)
mockUser.roles = roles
//模拟 UserDao对象
`when`(mockUserDao.getOne(1)).thenReturn(mockUser)
val u = userService.getOne(1)
println(ObjectMapper().writeValueAsString(u))
Assert.assertTrue(u?.password == "123456")
}
}
需要注意的是,该测试的执行 Runner 是 @RunWith(MockitoJUnitRunner::class) 。
使用 @Mock 注解标记这个对象是被 Mock 的。
使用 @InjectMocks 注解标注一个实现类UserServiceImpl,Mockito 会自动把 @Spy 或 @Mock标注的 Mock 对象注入到实现类UserServiceImpl的方法执行中,相当于把实现类中的UserDao对象使用mockUserDao对象给“偷梁换柱”了。
运行上面的测试类,可以发现测试成功
图15-2 MockUserServiceTest测试成功
在测试代码的打印日志中,输出的 getOne(1)方法的返回对象是我们 Mock 的对象mockUser :
{"id":101,"gmtCreate":"2018-02-09 01:48:33","gmtModify":"2018-02-09 01:48:33","username":"mockUser","password":"123456","roles":[{"id":-1,"gmtCreate":"2018-02-09 01:48:33","gmtModify":"2018-02-09 01:48:33","role":"ROLE_ADMIN"},{"id":-1,"gmtCreate":"2018-02-09 01:48:33","gmtModify":"2018-02-09 01:48:33","role":"ROLE_USER"}]}
提示:更多关于 Mockito 的使用请参考官网文档:http://site.mockito.org/
1.2.4 Controller 层测试
通过上面的实例,我们已经了解了在实际项目开发测试中对dao层代码和service层代码的测试,还学习了 Mockito 技术的相关内容。spring-boot-starter-test中提供了对项目测试功能的强大支持,更难得的是其中增加了对Controller层测试的支持。
下面我们来测试接口 http://127.0.0.1:8012/user/1 。该接口的输出的JSON数据如下
{
"id": 1,
"gmtCreate": "2018-02-08 12:58:14",
"gmtModify": "2018-02-08 12:58:14",
"username": "user",
"password": "user",
"roles": [
{
"id": 1,
"gmtCreate": "2018-02-08 12:58:14",
"gmtModify": "2018-02-08 12:58:14",
"role": "ROLE_USER"
}
]
}
UserControllerTest测试代码如下
@RunWith(SpringJUnit4ClassRunner::class)
@SpringBootTest
class UserControllerTest {
@Autowired
lateinit var context: WebApplicationContext
lateinit var mvc: MockMvc
@Before
fun setUp() {
mvc = MockMvcBuilders.webAppContextSetup(context).build()
}
@Test
fun testFetchUser1() {
mvc.perform(MockMvcRequestBuilders.get("/user/1")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("""
"username":"user"
""".trimIndent())))
.andDo {
println("it.request.method=${it.request.method}")
println("it.response.contentAsString=${it.response.contentAsString}")
}
.andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.equalTo(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.roles[0].role", Matchers.equalTo("ROLE_USER")))
}
}
其中, MockMvc是一个被final修饰的类型,该类无法被继承使用。这个类在包org.springframework.test.web.servlet下面,是Spring提供的模拟SpringMVC请求的实例类,该类由MockMvcBuilders通过WebApplicationContext实例进行创建。MockMvcBuilder接口签名如下
package org.springframework.test.web.servlet;
public interface MockMvcBuilder {
MockMvc build();
}
上面的代码简单说明如下表15-1。
表15-1
方法名
功能说明
Perform()
方法其实只是为了构建一个请求,并且返回ResultActions实例,使用该实例可以获取到请求的返回内容。
MockMvcRequestBuilders
支持构建多种请求方法对象,如:Post、Get、Put、Delete等常用的请求方式,其中的参数"/user/1"则是我们需要请求的本项目的相对路径,/ 则是项目请求的根路径。另外,还可以调用param() 方法用于在发送请求时携带参数。
andExpect()
是ResultActions中成员,入参是ResultMatcher类型:
ResultActions andExpect(ResultMatcher matcher)
在发送请求后对响应结果进行匹配校验时调用。其中MockMvcResultMatchers 抽象类是一个静态工厂,用于生产ResultMatcher对象。MockMvcResultMatchers中提供了丰富的匹配器。
1.2.5 JSON接口测试
使用 JsonPath 我们可以像 JavaScript 语法一样方便地进行 JSON 数据返回的访问操作。例如下面的这两行代码
.andExpect(MockMvcResultMatchers.jsonPath("$.id", Matchers.equalTo(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.roles[0].role", Matchers.equalTo("ROLE_USER")))
这里的Matchers类是org.hamcrest包下面的类。org.hamcrest.Matchers 类中提供了丰富的断言方法,这些方法的具体使用可以阅读Matchers 类的源码深入了解。
其中,"$.id" 和 "$.roles[0].role" 就是 JsonPath的表达式语法。
提示:更多关于 JsonPath 的内容可以参考: https://github.com/json-path/JsonPath 。
运行上面的测试代码,测试成功:
图15-3 UserControllerTest测试成功
使用命令 $ gradle test 可以一次性全部执行 src/test 目录下面的测试类。在 IDEA 中可以直接邮寄 src/test 目录,选择 Run > All Tests执行所有测试类,如下图所示
图15-4 选择 Run > All Tests执行 所有测试类
另外,Gradle Test 生成的测试报告在 build/reports/tests/test/index.html 中,如下图
图15-5 Gradle Test 生成的测试报告在 build/reports/tests/test/index.html 中
测试报告的部分内容截图如下
图15-6 测试报告Summary
图15-7 UserControllerTest测试报告
图15-8 MockUserServiceTest测试报告
1.3 本章小结
本章介绍了Spring Boot项目如何测试。Spring Boot 应用对Web层测试提供强大的支持:采用MockMvc方式测试Web请求,根据传递的不用参数以及请求返回对象反馈信息进行验证测试。另外,针对 JSON 数据接口,使用 JsonPath 可以方便地进行 JSON 数据结果的校验。
提示:本章项目工程源代码:
https://github.com/KotlinSpringBoot/demo_testing_and_deploy