基于spring-boot的应用程序的单元+集成测试方案 (2)

假设我们的测试目标如下:

@Service public class CityService { @Autowired private CityMapper cityMapper; public List<City> getAllCities() { return cityMapper.selectAllCities(); } public void save(City city) { cityMapper.insert(city); } }

我们可以这样编写测试类:

@RunWith(SpringRunner.class) @SpringBootTest public class CityServiceUnitTest { @SpringBootApplication(scanBasePackages = "com.shouzheng.demo.web") static class InnerConfig { } @Autowired private CityService cityService; @MockBean private CityMapper cityMapper; @Test public void testInsert() { City city = new City(); cityMapper.insert(city); Mockito.verify(cityMapper).insert(city); } @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("杭州"); } }

@RunWith注解声明测试是在spring环境下运行的,这样就可以启用Spring的相关支持。

@SpringBootTest注解负责扫描配置来构建测试用的Spring上下文环境。它默认搜索@SpringBootConfiguration类,除非我们通过classes属性指定配置类,或者通过自定义内嵌的@Configuration类来指定配置。如上面的代码,就是通过内嵌类来自定义配置。

@SpringBootApplication扩展自@Configuration,其scanBasePackages属性指定了扫描的根路径。确保测试目标类在这个路径下,而且需要明白这个路径下的所有bean都会被实例化。虽然我们已经尽可能的缩小了实例化的范围,但是我们没有避免其他无关类的实例化开销。

即使如此,这种方案依然被我看作是最佳的实践方案,因为它比较简单。如果我们追求“只实例化目标类”,那么可以使用下面的方式声明内嵌类:

@Configuration @ComponentScan(value = "com.shouzheng.demo.web", useDefaultFilters = false, includeFilters = @ComponentScan.Filter( type = FilterType.REGEX, pattern = {"com.shouzheng.demo.web.CityService"}) ) static class InnerConfig { }

@ComponentScan负责配置扫描Bean的方案,value属性指定扫描的根路径,useDefaultFilters属性取消默认的过滤器,includeFilters属性自定义了一个过滤器,这个过滤器设定为要扫描模式匹配的类。

@ComponentScan默认的过滤器会扫描@Component,@Repository,@Service,@Controller;如果不禁用默认过滤器,自定义过滤器的效果是在默认过滤器的基础上追加更多的bean。即我们要限定只实例化某个特定的bean,就需要把默认的过滤器禁用。

可以看到,这种扫描策略配置,会显得复杂很多。

@Autowired负责注入依赖的bean,在这里注入的是测试目标bean。

@MockBean负责声明这是一个模拟的bean。在进行单元测试时,需要将测试目标的所有依赖bean声明为模拟的bean,这些模拟的bean将被注入测试目标bean。

在testInsert方法中,我们执行了cityMapper.insert,这只是模拟的执行了,实际上什么也没做。接下来我们调用Mockito.verify,目的是验证cityMapper.insert执行了。这正对应了上文中对Mock概念的解释,我们只关心它是否执行了。

需要注意的是,验证的内容同时包括参数是否一致。如果实际调用时的传参和验证时指定的参数不一致,则验证失败,以至于测试失败。

在getAllCities方法中,我们使用Mockito.when对cityMapper.selectAllCities方法进行打桩,设定当方法被调用时,直接返回我们预设的数据。这也对应了上文中对Stub概念的解释。

注意:只能对mock对象进行stub

测试Controller

Controller是一类特殊的bean,这类bean除了显式的依赖,还有一些系统组件的依赖。比如消息转换组件,负责将方法的返回结果转换成可以写的HTTP消息。所以,我们无法像测试上文那样对其单独实例化。

Spring提供了特定的注解,配置用于测试Controller的上下文环境。

例如我们要测试的controller如下:

@RestController public class CityController { @Autowired private CityService cityService; @GetMapping("/cities") public ResponseEntity<?> getAllCities() { List<City> cities = cityService.getAllCities(); return ResponseEntity.ok(cities); } @PostMapping("/city") public ResponseEntity<?> newCity(@RequestBody City city) { cityService.save(city); return ResponseEntity.ok(city); } }

我们可以这样编写测试类:

@RunWith(SpringRunner.class) @WebMvcTest(CityController.class) public class CityControllerUnitTest { @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("杭州"))); } }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdfwd.html