基于Mockito的Spring Boot应用的测试
Spring Boot可以和大部分流行的测试框架协同工作:通过Spring JUnit创建单元测试;生成测试数据初始化数据库用于测试;Spring Boot可以跟BDD(Behavier Driven Development)工具、Cucumber和Spock协同工作,对应用程序进行测试。
进行软件开发的时候,我们会写很多代码,不过,再过六个月(甚至一年以上)你知道自己的代码怎么运作么?通过测试(单元测试、集成测试、接口测试)可以保证系统的可维护性,当我们修改了某些代码时,通过回归测试可以检查是否引入了新的bug。总得来说,测试让系统不再是一个黑盒子,让开发人员确认系统可用。
在web应用程序中,我们主要是对Service层做单元测试,对Controller层做集成测试或者接口测试,对Controller层的测试一般有两种方法:(1)发送http请求;(2)模拟http请求对象。第一种方法需要配置回归环境,通过修改代码统计的策略来计算覆盖率;第二种方法是比较正规的思路,下面我将演示如何用Mock对象测试Service、Controller层的代码。
在这里,我们就用上一篇文章中的实例,做为我们测试的项目,测试它提供的RESTful接口是否能返回正确的响应数据。当然,在测试前,需要为之初始化完整的应用程序上下文、所有的spring bean都织入以及数据库中需要有测试数据。
项目结构如下所示:

一.测试基础类
BaseMockTest.java
package com.bijian; import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes= UserSpringbootGradeDemoApplication.class) @WebAppConfiguration //@RunWith(SpringRunner.class) //@SpringBootTest(classes = UserSpringbootGradeDemoApplication.class,webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class BaseMockTest { }
注意:
1.较新版的Spring Boot取消了@SpringApplicationConfiguration这个注解,用@SpringBootTest就可以了,再用@SpringApplicationConfiguration就会报错(无法导入)
2.用上面注释的注解也可以,SpringRunner继承了SpringJUnit4ClassRunner,没有扩展任何功能;使用前者,名字简短而已
3.@RunWith(SpringJUnit4ClassRunner.class),这是JUnit的注解,通过这个注解让SpringJUnit4ClassRunner这个类提供Spring测试上下文。
4.@SpringBootTest(classes= UserSpringbootGradeDemoApplication.class),这是Spring Boot注解,为了进行集成测试,需要通过这个注解加载和配置Spring应用上下文。这是一个元注解(meta-annoation),它包含了@ContextConfiguration( loader = SpringApplicationContextLoader.class)这个注解,测试框架通过这个注解使用Spring Boot框架的SpringApplicationContextLoader加载器创建应用上下文。
5.@WebIntegrationTest("server.port:0"),这个注解表示当前的测试是集成测试(integration test),因此需要初始化完整的上下文并启动应用程序。这个注解一般和@SpringBootTest一起出现。server.port:0指的是让Spring Boot在随机端口上启动Tomcat服务,随后在测试中程序通过@Value("${local.server.port}")获得这个端口号,并赋值给port变量。当在Jenkins或其他持续集成服务器上运行测试程序时,这种随机获取端口的能力可以提供测试程序的并行性。
二.在build.gradle中增加spring-boot-starter-test等包依赖

三.服务层测试
package com.bijian.service; import com.bijian.BaseMockTest; import com.bijian.dao.OrderDao; import com.bijian.dto.AddOrderRsp; import com.bijian.entity.Book; import com.bijian.entity.Order; import com.bijian.entity.User; import com.bijian.enums.ReturnCodeEnum; import com.bijian.service.impl.OrderServiceImpl; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.beans.factory.annotation.Autowired; import java.math.BigDecimal; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class OrderServiceTest extends BaseMockTest { @Autowired @InjectMocks private OrderServiceImpl orderServiceImpl; @Mock private OrderDao mockOrderDao; @Mock private BookService mockBookService; @Mock private UserService mockUserService; @Before public void before() throws Exception { MockitoAnnotations.initMocks(this); } @Test public void test_addOrder_01_success() throws Exception { Order order = new Order(); order.setId(1); order.setUserId(2); order.setBookId(2); order.setBookNum(2); String mockUserName = "lisi"; String mockBookName = "javaScript"; BigDecimal mockBookPrice = new BigDecimal("10.2"); User mockUser = new User(); mockUser.setName(mockUserName); when(mockUserService.findUserById(order.getUserId())).thenReturn(mockUser); Book mockBook = new Book(); mockBook.setName(mockBookName); mockBook.setPrice(mockBookPrice); mockBook.setRemainNum(100); when(mockBookService.findBookById(order.getBookId())).thenReturn(mockBook); System.out.println(mockBook.getName()); when(mockOrderDao.addOrder(order)).thenReturn(1); AddOrderRsp result = orderServiceImpl.addOrder(order); verify(mockBookService, times(1)).updateBook(Mockito.any(Book.class)); Assert.assertEquals(ReturnCodeEnum.SUCCESS.getCode(), result.getRetCode()); } @Test public void test_addOrder_success() throws Exception { Order order = new Order(); order.setId(1); order.setUserId(2); order.setBookId(2); order.setBookNum(2); String mockUserName = "lisi"; String mockBookName = "javaScript"; BigDecimal mockBookPrice = new BigDecimal("10.2"); User mockUser = new User(); mockUser.setName(mockUserName); when(mockUserService.findUserById(Mockito.any(Integer.class))).thenReturn(mockUser); Book mockBook = new Book(); mockBook.setName(mockBookName); mockBook.setPrice(mockBookPrice); mockBook.setRemainNum(100); when(mockBookService.findBookById(Mockito.any(Integer.class))).thenReturn(mockBook); when(mockOrderDao.addOrder(Mockito.any(Order.class))).thenAnswer(new Answer<Integer>() { @Override public Integer answer(InvocationOnMock invo) { Object[] args = invo.getArguments(); Order reqOrder = (Order)args[0]; Assert.assertEquals(mockBookName, reqOrder.getBookName()); Assert.assertEquals(mockUserName, reqOrder.getUserName()); Assert.assertEquals(mockBookPrice, reqOrder.getBookPrice()); return 1; } }); AddOrderRsp result = orderServiceImpl.addOrder(order); verify(mockBookService, times(1)).updateBook(Mockito.any(Book.class)); Assert.assertEquals(ReturnCodeEnum.SUCCESS.getCode(), result.getRetCode()); } }
这里用到的知识点都是mockito相关的知识点,就是打桩、测试、验证。
四.控制层测试
package com.bijian.controller; import com.bijian.BaseMockTest; import com.bijian.dto.OrderRsp; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @AutoConfigureMockMvc public class OrderRestControllerTest extends BaseMockTest { private MockMvc mockMvc; private TestRestTemplate restTemplate = new TestRestTemplate(); @Autowired private WebApplicationContext context; @Before public void setUp(){ mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test public void test_webapp_order_api_01() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/api/order/orderId?orderId=1") .accept(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(content().string(containsString("{\"order\":{\"id\":1,\"userId\":2,\"userName\":\"wangjinxiang\",\"bookId\":2,\"bookName\":\"javaScript\",\"bookNum\":2,\"bookPrice\":20.80},\"remainNum\":0,\"nowPrice\":20.80}"))) .andExpect(jsonPath("$.order.userName").value("wangjinxiang")); } @Test public void test_webapp_order_api_02() { OrderRsp orderRsp = restTemplate.getForObject("http://localhost:" + 8080 + "/api/order/orderId?orderId=1", OrderRsp.class); assertNotNull(orderRsp); assertEquals("wangjinxiang", orderRsp.getOrder().getUserName()); assertEquals("javaScript", orderRsp.getOrder().getBookName()); } }
注意:
1.由于这是Spring Boot的测试,因此我们可通过@Autowired注解织入任何由Spring管理的对象,或者是通过@Value设置指定的环境变量的值。在现在这个测试类中,我们定义了WebApplicationContext对象。
2.每个测试用例用@Test注解修饰。
3.第一个测试用例中展示了如何通过MockMvc对象实现对RESTful URL接口订单查询的测试。Spring测试框架提供MockMvc对象,可以在不需要客户端-服务端请求的情况下进行MVC测试,完全在服务端这边就可以执行Controller的请求,跟启动了测试服务器一样。
4.第二个测试用例也是用来测试我们提供的RESTful URL接口订单查询,即“/api/order/orderId?orderId={orderId}”。在这个测试用例中我们使用TestRestTemplate对象发起RESTful请求。
5.测试开始之前需要建立测试环境,setup方法被@Before修饰。通过MockMvcBuilders工具,使用WebApplicationContext对象作为参数,创建一个MockMvc对象。
6.MockMvc对象提供一组工具函数用来执行assert判断,都是针对web请求的判断。这组工具的使用方式是函数的链式调用,允许程序员将多个测试用例链接在一起,并进行多个判断。在这个例子中我们用到下面的一些工具函数:
a.perform(get(...))建立web请求。在我们的第一个用例中,通过MockMvcRequestBuilder执行GET请求。
b.andExpect(...)可以在perform(...)函数调用后多次调用,表示对多个条件的判断,这个函数的参数类型是ResultMatcher接口,在MockMvcResultMatchers这这个类中提供了很多返回ResultMatcher接口的工具函数。这个函数使得可以检测同一个web请求的多个方面,包括HTTP响应状态码(response status),响应的内容类型(content type),会话中存放的值,检验重定向、model或者header的内容等等。如需要通过json-path检测JSON格式的响应数据:检查json数据包含正确的元素类型和对应的值,例如jsonPath("$.order.userName").value("wangjinxiang")用于检查在根目录下的order下有一个名为userName的节点,并且该节点对应的值是“wangjinxiang”。
7.一个字符乱码问题
问题描述:通过spring-boot-starter-data-rest建立的repository,取出的汉字是乱码。
分析:使用postman和httpie验证都没问题,说明是Mockmvc的测试用例写得不对,应该主动设置客户端如何解析HTTP响应,用get.accept方法设置客户端可识别的内容类型,参考测试用例如下:
@Test public void webappPublisherApi() throws Exception { //MockHttpServletRequestBuilder.accept方法是设置客户端可识别的内容类型 //MockHttpServletRequestBuilder.contentType,设置请求头中的Content-Type字段,表示请求体的内容类型 mockMvc.perform(get("/publishers/1") .accept(MediaType.APPLICATION_JSON_UTF8)) .andExpect(status().isOk()) .andExpect(content().string(containsString("中文测试"))) .andExpect(jsonPath("$.name").value("中文测试")); }
项目完整代码见《SpringBoot小项目实例》。
参考文章:https://www.cnblogs.com/alliswelltome/p/5948973.html
https://blog.csdn.net/limenghua9112/article/details/79694849
https://blog.csdn.net/xupeng874395012/article/details/75087907
posted on 2019-01-08 23:13 bijian1013 阅读(799) 评论(0) 收藏 举报
浙公网安备 33010602011771号