本文主要介绍如何对基于spring-boot的web应用编写单元测试、集成测试的代码。html
此类应用的架构图通常以下所示:java
咱们项目的程序,对应到上图中的web应用部分。这部分通常分为Controller层、service层、持久层。除此以外,应用程序中还有一些数据封装类,咱们称之为domain。上述各组件的职责以下:mysql
在Spring环境中,咱们一般会把这三层注册到Spring容器,上图中使用浅蓝色背景就是为了表示这一点。git
在本文的后续内容,咱们将介绍如何对应用进行集成测试,包括启动web容器的请求测试、不启动web容器而使用模拟环境的测试;介绍如何对应用进行单元测试,包括单独测试Controller层、service层、持久层。github
集成测试和单元测试的区别是,集成测试一般只须要测试最上面一层,由于上层会自动调用下层,因此会测试完整的流程链,流程链中每个环节都是真实、具体的。单元测试是单独测试流程链中的某一环,这一个环所直接依赖的下游环节使用模拟的方式来提供支撑,这一技术称为Mock。在介绍单元测试的时候,咱们会介绍如何mock依赖对象,并简单对mock的原理进行介绍。web
本文所关注的另外一个主题,是在持久层测试时,如何消除修改数据库的反作用。redis
集成测试是在全部组件都已经开发完成以后,进行组装测试。有两种测试方式:启动web容器进行测试,使用模拟环境测试。这两种测试的效果没有什么差异,只是使用模拟环境测试的话,能够不用启动web容器,从而会少一些开销。另外,二者的测试API会有所不一样。spring
咱们经过测试最上层的Controller来实施集成测试,咱们的测试目标以下:sql
@RestController public class CityController { @Autowired private CityService cityService; @GetMapping("/cities") public ResponseEntity<?> getAllCities() { List<City> cities = cityService.getAllCities(); return ResponseEntity.ok(cities); } }
这是一个Controller,它对外提供一个服务/cities
,返回一个包含全部城市的列表。这个Controller经过调用下一层的CityService来完成本身的职责。数据库
针对这个Controller的集成测试方案以下:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class CityControllerWithRunningServer { @Autowired private TestRestTemplate restTemplate; @Test public void getAllCitiesTest() { String response = restTemplate.getForObject("/cities", String.class); Assertions.assertThat(response).contains("San Francisco"); } }
首先咱们使用@RunWith(SpringRunner.class)
声明在Spring的环境中进行单元测试,这样Spring的相关注解才会被识别并起效。而后咱们使用@SpringBootTest,它会扫描应用程序的spring配置,并构建完整的Spring Context。咱们为其参数webEnvironment赋值为SpringBootTest.WebEnvironment.RANDOM_PORT,这样就会启动web容器,并监听一个随机的端口,同时,为咱们自动装配一个TestRestTemplate类型的bean来辅助咱们发送请求。
测试的目标不变,测试的方案以下:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class CityControllerWithMockEnvironment { @Autowired private MockMvc mockMvc; @Test public void getAllCities() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/cities")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("San Francisco"))); } }
咱们依然使用@SpringBootTest
,可是没有设置其webEnvironment
属性,这样依然会构建完整的Spring Context,可是不会再启动web容器。为了进行测试,咱们须要使用MockMvc
实例发送请求,而咱们使用@AutoConfigureMockMvc
则是由于这样能够得到自动配置的MockMvc
实例。
具体测试的代码中出现不少新的API,对于API细节的研究不在本文计划范围内。
上文中描述的两种集成测试的方案,相同的一点是都会构建整个Spring Context。这表示全部声明的bean,而无论声明的方式为什么,都会被构建实例,而且都能被依赖。这里隐含的意思是从上到下整条依赖链上的代码都已实现。
Mock技术
在开发的过程当中进行测试,没法知足上述的条件,Mock技术可让咱们屏蔽掉下层的依赖,从而专一于当前的测试目标。Mock技术的思想是,当测试目标的下层依赖的行为是可预期的,那么测试目标自己的行为也是可预期的,测试就是把实际的结果和测试目标的预期结果作比较,而Mock就是预先设定下层依赖的行为表现。
Mock的流程
Mock的使用场景
测试的目标不变,测试的方案以下:
/** * 不构建整个Spring Context,只构建指定的Controller进行测试。须要对相关的依赖进行mock.<br> * Created by lijinlong9 on 2018/8/22. */ @RunWith(SpringRunner.class) @WebMvcTest(CityController.class) public class CityControllerWebLayer { @Autowired private MockMvc mvc; @MockBean private CityService service; @Test public void getAllCities() throws Exception { City city = new City(); city.setId(1L); city.setName("杭州"); city.setState("浙江"); city.setCountry("中国"); Mockito.when(service.getAllCities()).thenReturn(Collections.singletonList(city)); mvc.perform(MockMvcRequestBuilders.get("/cities")) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("杭州"))); } }
这里再也不使用@SpringBootTest
,而代之以@WebMvcTest
,这样只会构建web层或者指定的一到多个Controller的bean。@WebMvcTest
一样能够为咱们自动配置MockMvc
类型的bean,咱们可使用它来模拟发送请求。
@MockBean
是一个新接触的注解,它表示对应的bean是一个模拟的bean。由于咱们要测试CityController
,对其依赖的CityService
,咱们须要mock其预期的行为表现。在具体的测试方法中,使用Mockito的API对sercive的行为进行mock,它表示当调用service的getAllCities时,会返回预先设定的一个City对象的列表。
以后就是发起请求,并预测结果。
Mockito是Java语言的mock测试框架,spring以本身的方式集成了它。
持久层的测试方案跟具体的持久层技术相关。这里咱们介绍基于Mybatis的持久层的测试。
测试目标是:
@Mapper public interface CityMapper { City selectCityById(int id); List<City> selectAllCities(); int insert(City city); }
测试方案是:
@RunWith(SpringRunner.class) @MybatisTest @FixMethodOrder(value = MethodSorters.NAME_ASCENDING) // @Transactional(propagation = Propagation.NOT_SUPPORTED) public class CityMapperTest { @Autowired private CityMapper cityMapper; @Test public void /*selectCityById*/ test1() throws Exception { City city = cityMapper.selectCityById(1); Assertions.assertThat(city.getId()).isEqualTo(Long.valueOf(1)); Assertions.assertThat(city.getName()).isEqualTo("San Francisco"); Assertions.assertThat(city.getState()).isEqualTo("CA"); Assertions.assertThat(city.getCountry()).isEqualTo("US"); } @Test public void /*insertCity*/ test2() throws Exception { City city = new City(); city.setId(2L); city.setName("HangZhou"); city.setState("ZheJiang"); city.setCountry("CN"); int result = cityMapper.insert(city); Assertions.assertThat(result).isEqualTo(1); } @Test public void /*selectNewInsertedCity*/ test3() throws Exception { City city = cityMapper.selectCityById(2); Assertions.assertThat(city).isNull(); } }
这里使用了@MybatisTest
,它负责构建mybatis-mapper层的bean,就像上文中使用的@WebMvcTest
负责构建web层的bean同样。值得一提的是@MybatisTest
来自于mybatis-spring-boot-starter-test
项目,它是mybatis团队根据spring的习惯来实现的。Spring原生支持的两种持久层的测试方案是@DataJpaTest
和@JdbcTest
,分别对应JPA持久化方案和JDBC持久化方案。
@FixMethodOrder
来自junit,目的是为了让一个测试类中的多个测试方案按照设定的顺序执行。通常状况下不须要如此,我这里想确认test2方法中插入的数据,在test3中是否还存在,因此须要保证二者的执行顺序。
咱们注入了CityMapper
,由于其没有更底层的依赖,因此咱们不须要进行mock。
@MybatisTest
除了实例化mapper相关的bean以外,还会检测依赖中的内嵌数据库,而后测试的时候使用内嵌数据库。若是依赖中没有内嵌数据库,就会失败。固然,使用内嵌数据库是默认的行为,可使用配置进行修改。
@MybatisTest
还会确保每个测试方法都是事务回滚的,因此在上述的测试用例中,test2插入了数据以后,test3中依然获取不到插入的数据。固然,这也是默认的行为,能够改变。
service层并不做为一种特殊的层,因此没有什么注解能表示“只构建service层的bean”这种概念。
这里将介绍另外一种通用的测试场景,我要测试的是一个普通的bean,没有什么特殊的角色,好比不是担当特殊处理的controller,也不是负责持久化的dao组件,咱们要测试的只是一个普通的bean。
上文中咱们使用@SpringBootTest
的默认机制,它去查找@SpringBootApplication
的配置,据此构建Spring的上下文。查看@SpringBootTest
的doc,其中有一句是:
Automatically searches for a @SpringBootConfiguration when nested @Configuration is not used, and no explicit classes are specified.
这表示咱们能够经过classes属性来指定Configuration类,或者定义内嵌的Configuration类来改变默认的配置。
在这里咱们经过内嵌的Configuration类来实现,先看下测试目标 - CityService:
@Service public class CityService { @Autowired private CityMapper cityMapper; public List<City> getAllCities() { return cityMapper.selectAllCities(); } }
测试方案:
@RunWith(SpringRunner.class) @SpringBootTest public class CityServiceTest { @Configuration static class CityServiceConfig { @Bean public CityService cityService() { return new CityService(); } } @Autowired private CityService cityService; @MockBean private CityMapper cityMapper; @Test public void getAllCities() { City city = new City(); city.setId(1L); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); Mockito.when(cityMapper.selectAllCities()) .thenReturn(Collections.singletonList(city)); List<City> result = cityService.getAllCities(); Assertions.assertThat(result.size()).isEqualTo(1); Assertions.assertThat(result.get(0).getName()).isEqualTo("杭州"); } }
一样的,对于测试目标的依赖,咱们须要进行mock。
单元测试中,须要对测试目标的依赖进行mock,这里有必要对mock的细节介绍下。上文单元测试部分已对Mock的逻辑、流程和使用场景进行了介绍,此处专一于实践层面进行说明。
通常的mock是对方法级别的mock,在方法有入参的状况下,方法的行为可能会跟方法的具体参数值有关。好比一个除法的方法,传入参数四、2得结果2,传入参数八、2得结果4,传入参数二、0得异常。
mock能够针对不一样的参数值设定不一样的预期,以下所示:
@RunWith(SpringRunner.class) @SpringBootTest public class MathServiceTest { @Configuration static class ConfigTest {} @MockBean private MathService mathService; @Test public void testDivide() { Mockito.when(mathService.divide(4, 2)) .thenReturn(2); Mockito.when(mathService.divide(8, 2)) .thenReturn(4); Mockito.when(mathService.divide(ArgumentMatchers.anyInt(), ArgumentMatchers.eq(0))) // 必须同时用matchers语法 .thenThrow(new RuntimeException("error")); Assertions.assertThat(mathService.divide(4, 2)) .isEqualTo(2); Assertions.assertThat(mathService.divide(8, 2)) .isEqualTo(4); Assertions.assertThatExceptionOfType(RuntimeException.class) .isThrownBy(() -> { mathService.divide(3, 0); }) .withMessageContaining("error"); } }
上面的测试可能有些奇怪,mock的对象也同时做为测试的目标。这是由于咱们的目的在于介绍mock,因此简化了测试流程。
从上述测试用例能够看出,咱们除了能够指定具体参数时的行为,也能够指定参数知足必定匹配规则时的行为。
对于有返回的方法,mock时能够设定的行为有:
返回设定的结果,如:
when(taskService.findResourcePool(any())) .thenReturn(resourcePool);
直接抛出异常,如:
when(taskService.createTask(any(), any(), any())) .thenThrow(new RuntimeException("zz"));
实际调用真实的方法,如:
when(taskService.createTask(any(), any(), any())) .thenCallRealMethod();
注意,调用真实的方法有违mock的本义,应该尽可能避免。若是要调用的方法中调用了其余的依赖,须要自行注入其余的依赖,不然会空指针。
对于无返回的方法,mock时能够设定的行为有:
直接抛出异常,如:
doThrow(new RuntimeException("test")) .when(taskService).saveToDBAndSubmitToQueue(any());
实际调用(下列为Mockito类的doc中给出的示例,我并无遇到此需求),如:
doAnswer(new Answer() { public Object answer(InvocationOnMock invocation) { Object[] args = invocation.getArguments(); Mock mock = invocation.getMock(); return null; }}) .when(mock).someMethod();
@RunWith
:SpringRunner.class
,可以将junit和spring进行集成。后续的spring相关注解才会起效。@SpringBootTest
:@AutoConfigureMockMvc
:MockMvc
对象实例,用来在模拟测试环境中发送http请求。@WebMvcTest
:@SpringBootTest
能将构建bean的范围限定于web层,可是web层的下层依赖bean,须要经过mock来模拟。也能够经过参数指定只实例化web层的某一个到多个controller。具体可参考Auto-configured Spring MVC Tests。@RestClientTest
:@MybatisTest
:@SpringBootTest
,可以将构建bean的返回限定于mybatis-mapper层。具体可参考mybatis-spring-boot-test-autoconfigure。@JdbcTest
:JdbcTemplate
),那么可使用该注解代替@SpringBootTest
,限定bean的构建范围。官方参考资料有限,可自行网上查找资料。@DataJpaTest
:@DataRedisTest
:给持久层测试类添加注解@AutoConfigureTestDatabase(replace = Replace.NONE)
可使用配置的数据库做为测试数据库。同时,须要在配置文件中配置数据源,以下:
spring: datasource: url: jdbc:mysql://127.0.0.1/test username: root password: root driver-class-name: com.mysql.jdbc.Driver
能够在测试方法上添加@Rollback(false)
来设置不回滚,也能够在测试类的级别上添加该注解,表示该类全部的测试方法都不会回滚。