专栏首页一个会写诗的程序员的博客Spring Boot 应用的测试Spring Boot 应用的测试

Spring Boot 应用的测试Spring Boot 应用的测试

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 《Spring Boot 实战:从0到1》第1章 Spring Boot简介第1章 Spring Boot简介小结参考资料

    Java Web开发涉及的技术比较繁杂,涉及到很多开发框架和工具(Java, Scala, Kotlin, Clojure,Groovy, Grails,Gr...

    一个会写诗的程序员
  • 第2章 Spring Boot简介小结参考资料

    Java Web开发涉及的技术比较繁杂,涉及到很多开发框架和工具(Java, Scala, Kotlin, Clojure,Groovy, Grails,Gr...

    一个会写诗的程序员
  • 《Spring Boot极简教程》第2章 Spring Boot简史小结参考资料

    Java Web开发涉及的技术比较繁杂,涉及到很多开发框架和工具(Java, Scala, Kotlin, Clojure,Groovy, Grails,Gr...

    一个会写诗的程序员
  • Java学习记录——探究Spring Boot与Spring Cloud之间的关系

    SpringBoot相当于脚手架,借助他可以快速搭建房子,它本身不具备任何功能属性,只是普通房间,没有其他任何功能。

    慕容千语
  • Spring Cloud-微服务架构集大成者

    本文不是讲解如何使用Spring Cloud的教程,而是探讨Spring Cloud是什么,以及它诞生的背景和意义。

    爱撸猫的杰
  • Spring 十个错误的使用姿势!

    我们正在解决这个常见错误,是因为 “非我所创” 综合症在软件开发领域很是常见。症状包括经常重写一些常见的代码,很多开发人员都有这种症状。

    江南一点雨
  • 精讲Spring Boot—入门+进阶+实例

    在了解Spring Boot之前应该先了解下Spring,因为Spring Boot的核心是基于Spring构建的。Spring是由Rod Johnson在20...

    java乐园
  • Spring Cloud在国内中小型公司能用起来吗?

    今天吃完饭休息的时候瞎逛知乎,突然看到这个一个问题《Spring Cloud在国内中小型公司能用起来吗?》,吸引了我的注意。仔细的看了题主的问题,发现这是一个好...

    纯洁的微笑
  • pytest文档12-skip跳过用例

    pytest.mark.skip可以标记无法在某些平台上运行的测试功能,或者您希望失败的测试功能

    上海-悠悠
  • Spring Boot 和 Vue 前后端分离教程(附源码)

    来源:https://juejin.im/post/5c622fb5e51d457f9f2c2381

    Java团长

扫码关注云+社区

领取腾讯云代金券