概述

本文主要介绍单元测试、集成测试相关的概念、技术实现以及最佳实践。

本文的demo是基于Java语言,Spring Boot构建的web应用。测试框架使用Junit,模拟框架使用mockito。

之前曾经总结过一篇文章:基于spring-boot的应用程序的单元测试方案,但是当时只是从技术实现的角度去研究单元测试,很多概念没有搞清楚。本文在重新梳理脉络,丰富概念的基础上,整合了前文的大部分内容,但是有一部分几乎在实践中用不到的内容就被删去了。

在我的个人wiki站点,可以获得更好的阅读体验喔:基于spring-boot的应用程序的单元+集成测试方案

概念解析

单元测试和集成测试

测试领域有很多场景,比如单元测试,集成测试,系统测试,冒烟测试,回归测试,端到端测试,功能测试等。测试的分类方式各有不同,一些测试场景也可能存在重叠。具体这些场景的概念和区别,大家可以阅读文末给出的参考资料。

这里主要以程序员的视角谈一下我理解的单元测试和集成测试。

单元测试是编写单元测试类,针对类级别的测试。比如使用Junit框架,针对一个类,写一个测试类,测试目标类的大部分主要方法。

需要注意单元测试的级别是类。项目当中,类之间的依赖调用是很常见的事,如果你要测试一个类,而这个目标类又调用了另一个类,那么在测试时就没有遵守“在一个类范围内进行测试”,自然算不得单元测试。

如上图所示,假设A,B,C,D四个类之间存在上述的依赖关系,我们要测试类A,那么如何遵守“在类A的范围内测试”?

这就是模拟框架要解决的问题了,通过模拟B和C,我们可以在测试A的时候,调用B和C的模拟对象,而不是实际的B和C。下文会有详细介绍。

如果在测试时超脱一个类的范围,那就可以称为集成测试。如上图所示,你可以测试类A,它会直接或间接调用其他三个类,这就可以叫做集成测试。如果你去测试类C,因为它会调用D,也可以称为集成测试。

如果纯粹按照单元测试的概念,把这个工作代入到一个大型的项目,成百上千的类需要编写测试类,而且类之间的依赖需要编写模拟代码。这样的工作太过庞大,对项目来说应该是得不偿失的。

我推荐的做法是识别核心代码,或者说是重要的代码,只对这些代码做精细的单元测试。除此之外,都通过集成测试来覆盖。集成测试时优先从最顶层开始,让测试自然流下来。然后根据代码测试覆盖报告,再进行补刀。

Mock和Stub

此处介绍的mock和stub,是作者基于mockito框架的理解,行业内对这两个概念的定义和此处的理解可能有所出入。作者不追求对概念有“专业的定义”或者“精确的定义”,如果读者有此追求,可另外查阅其他资料。

上文讲到,在做单元测试的时候,需要屏蔽目标类的依赖,mock和stub就是这种操作涉及到的两个概念。

在项目代码中,经常会涉及依赖多个外部资源的情况,比如数据库、微服务中的其他服务。这表示在测试的时候需要先做很多准备工作,比如准备数据库环境,比如先把依赖的服务run起来。

另外,还需要考虑消除测试的副作用,以使测试具备幂等性。比如如果测试会修改数据库,那么是否会影响二次测试的结果,或者影响整个测试环境?

对外部的资源依赖进行模拟,是一个有效的解决方案。即测试时不是真正的操作外部资源,而是通过自定义的代码进行模拟操作。我们可以对任何的依赖进行模拟,从而使测试的行为不需要任何准备工作或者不具备任何副作用。

在这个大环境下,可以解释mock和stub的含义。当我们在测试时,如果只关心某个操作是否执行过,而不关心这个操作的具体行为,这种技术称为mock。

比如我们测试的代码会执行发送邮件的操作,我们对这个操作进行mock;测试的时候我们只关心是否调用了发送邮件的操作,而不关心邮件是否确实发送出去了。

另一种情况,当我们关心操作的具体行为,或者操作的返回结果的时候,我们通过执行预设的操作来代替目标操作,或者返回预设的结果作为目标操作的返回结果。这种对操作的模拟行为称为stub(打桩)。

比如我们测试代码的异常处理机制是否正常,我们可以对某处代码进行stub,让它抛出异常。再比如我们测试的代码需要向数据库插入一条数据,我们可以对插入数据的代码进行stub,让它始终返回1,表示数据插入成功。

技术实现

单元测试

测试常规的bean

当我们进行单元测试的时候,我们希望在spring容器中只实例化测试目标类的实例。

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

  1. @Service
  2. public class CityService {
  3. @Autowired
  4. private CityMapper cityMapper;
  5. public List<City> getAllCities() {
  6. return cityMapper.selectAllCities();
  7. }
  8. public void save(City city) {
  9. cityMapper.insert(city);
  10. }
  11. }

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

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class CityServiceUnitTest {
  4. @SpringBootApplication(scanBasePackages = "com.shouzheng.demo.web")
  5. static class InnerConfig { }
  6. @Autowired
  7. private CityService cityService;
  8. @MockBean
  9. private CityMapper cityMapper;
  10. @Test
  11. public void testInsert() {
  12. City city = new City();
  13. cityMapper.insert(city);
  14. Mockito.verify(cityMapper).insert(city);
  15. }
  16. @Test
  17. public void getAllCities() {
  18. City city = new City();
  19. city.setId(1L);
  20. city.setName("杭州");
  21. city.setState("浙江");
  22. city.setCountry("CN");
  23. Mockito.when(cityMapper.selectAllCities())
  24. .thenReturn(Collections.singletonList(city));
  25. List<City> result = cityService.getAllCities();
  26. Assertions.assertThat(result.size()).isEqualTo(1);
  27. Assertions.assertThat(result.get(0).getName()).isEqualTo("杭州");
  28. }
  29. }

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

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

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

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

  1. @Configuration
  2. @ComponentScan(value = "com.shouzheng.demo.web",
  3. useDefaultFilters = false,
  4. includeFilters = @ComponentScan.Filter(
  5. type = FilterType.REGEX,
  6. pattern = {"com.shouzheng.demo.web.CityService"})
  7. )
  8. 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.whencityMapper.selectAllCities方法进行打桩,设定当方法被调用时,直接返回我们预设的数据。这也对应了上文中对Stub概念的解释。

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

测试Controller

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

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

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

  1. @RestController
  2. public class CityController {
  3. @Autowired
  4. private CityService cityService;
  5. @GetMapping("/cities")
  6. public ResponseEntity<?> getAllCities() {
  7. List<City> cities = cityService.getAllCities();
  8. return ResponseEntity.ok(cities);
  9. }
  10. @PostMapping("/city")
  11. public ResponseEntity<?> newCity(@RequestBody City city) {
  12. cityService.save(city);
  13. return ResponseEntity.ok(city);
  14. }
  15. }

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

  1. @RunWith(SpringRunner.class)
  2. @WebMvcTest(CityController.class)
  3. public class CityControllerUnitTest {
  4. @Autowired
  5. private MockMvc mvc;
  6. @MockBean
  7. private CityService service;
  8. @Test
  9. public void getAllCities() throws Exception {
  10. City city = new City();
  11. city.setId(1L);
  12. city.setName("杭州");
  13. city.setState("浙江");
  14. city.setCountry("中国");
  15. Mockito.when(service.getAllCities()).
  16. thenReturn(Collections.singletonList(city));
  17. mvc.perform(MockMvcRequestBuilders.get("/cities"))
  18. .andDo(MockMvcResultHandlers.print())
  19. .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("杭州")));
  20. }
  21. }

@WebMvcTest是特定的注解,它的职责和@SpringBootTest相同,但它只会实例化Controller。默认实例化所有的Controller,也可以指定只实例化某一到多个Controller。

除此之外,@WebMvcTest还会实例化一个MockMvc的bean,用于发送http请求。

我们同样需要对测试目标的依赖进行模拟,即,将CityService声明为MockBean。

spring环境问题

@WebMvcTest就像@SpringBootTest一样,默认搜索@SpringBootConfiguration注解的类作为配置类。一般情况下,基于Spring-Boot的web应用,会创建一个启动类,并使用@SpringBootApplication,这个注解可看作@SpringBootConfiguration注解的扩展,所以很可能会搜索到这个启动类作为配置。

如果项目当中有多个@SpringBootConfiguration配置类,比如有些其他的测试类创建了内部配置类,并且使用了这个注解。如果当前测试类没有使用内部类,也没有使用classes属性指定使用哪个配置类,就会因为找到了多个配置类而失败。这种情况下会有明确的错误提示信息。

思考当前测试类会使用哪一个配置类,是一个很好的习惯。

另外一个可能的问题是:如果配置类上添加了其他的注解,比如Mybatis框架的@MapperScan注解,那么Spring会去尝试实例化Mapper实例,但是因为我们使用的是@WebMvcTest注解,Spring不会去实例化Mapper所依赖的sqlSessionFactory等自动配置的组件,最终导致依赖注解失败,无法构建Spring上下文环境。

也就是说,虽然@WebMvcTest默认只实例化Controller组件,但是它同样也会遵从配置类的注解去做更多的工作。如果这些工作依赖于某些自动化配置bean,那么将会出现依赖缺失。

解决这个问题的方法可能有很多种,我这边提供一个自己的最佳实践:

  1. @RunWith(SpringRunner.class)
  2. @WebMvcTest(CityController.class)
  3. public class CityControllerWebLayer {
  4. @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.web"})
  5. static class InnerConfig {}
  6. @Autowired
  7. private MockMvc mvc;
  8. @MockBean
  9. private CityService service;
  10. }

这个方案,是通过使用内部类来自定义配置。内部类只有一个@SpringBootApplication注解,指定了扫描的根路径,以缩小bean的扫描范围。

测试持久层

就像测试controller一样,持久层的单元测试也有专门的注解支持。

持久层的技术有多种,Spring提供了@JdbcTest来支持通过spring的JdbcTemplate进行持久化的测试,提供了@DataJpsTest支持通过JPA技术进行持久化的测试。

上面的这两个注解我没有做过研究,因为项目中使用的是Mybatis,这里仅介绍Mybatis提供的测试支持:@MybatisTest

最简单的方式是使用内存数据库作为测试数据库,这样可以尽量减少测试的环境依赖。

默认的持久层测试是回滚的,即每一个测试方法执行完成之后,会回滚对数据库的修改;所以也可以使用外部的数据库进行测试,但多少会有些影响(比如序列的当前值)。

使用内存数据库

首先,添加数据库依赖:

  1. <!-- pom.xml -->
  2. <dependency>
  3. <groupId>com.h2database</groupId>
  4. <artifactId>h2</artifactId>
  5. <version>RELEASE</version>
  6. <scope>test</scope>
  7. </dependency>

准备数据库初始化脚本,比如放在resources/import.sql文件中:

  1. drop table if exists city;
  2. drop table if exists hotel;
  3. create table city (id int primary key AUTO_INCREMENT, name varchar, state varchar, country varchar);
  4. create table hotel (city int primary key AUTO_INCREMENT, name varchar, address varchar, zip varchar);
  5. insert into city (id, name, state, country) values (1, 'San Francisco', 'CA', 'US');
  6. insert into hotel(city, name, address, zip) values (1, 'Conrad Treasury Place', 'William & George Streets', '4001')

需要在配置文件中指定脚本文件的位置:

  1. spring.datasource.schema=classpath:import.sql

例如我们要测试如下的Mapper接口:

  1. @Mapper
  2. public interface CityMapper {
  3. City selectCityById(int id);
  4. List<City> selectAllCities();
  5. int insert(City city);
  6. }

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

  1. @RunWith(SpringRunner.class)
  2. @MybatisTest
  3. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
  4. public class CityMapperUnitTest {
  5. @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.mapper"})
  6. static class InnerConfig {}
  7. private static Logger LOG = LoggerFactory.getLogger(CityMapperUnitTest.class);
  8. @Autowired
  9. private CityMapper cityMapper;
  10. @Before
  11. @After
  12. public void printAllCities() {
  13. List<City> cities = cityMapper.selectAllCities();
  14. LOG.info("{}", cities);
  15. }
  16. @Test
  17. // @Rollback(false) // 禁止回滚
  18. public void test1_insert() throws Exception {
  19. City city = new City();
  20. city.setName("杭州");
  21. city.setState("浙江");
  22. city.setCountry("CN");
  23. cityMapper.insert(city);
  24. LOG.info("insert a city {}", city);
  25. }
  26. @Test
  27. public void test2_doNothing() {
  28. }
  29. }

@MybatisTest搜索配置类的逻辑和@SpringBootTest@WebMvcTest相同,为了避免Spring环境问题(上文在测试Controller一节中介绍过),这里直接使用内部类进行配置。

@FixMethodOrder(MethodSorters.NAME_ASCENDING)用来指定测试方法的执行顺序,这是为了观察事务回滚的效果。

如果将test1_insert方法上的@Rollback(false)注释放开,事务不会回滚,test2_doNothing方法之后打印输出的内容会包含test1_insert方法里插入的数据。

反之,如果注释掉,事务回滚,test2_doNothing方法之后打印输出的内容不包含test1_insert方法里插入的数据。

使用外部数据库

首先,添加对应的数据库驱动依赖,以及数据源配置。比如使用mysql外部数据库:

  1. <!-- pom.xml -->
  2. <dependency>
  3. <groupId>mysql</groupId>
  4. <artifactId>mysql-connector-java</artifactId>
  5. <version>${mysql-jdbc.version}</version>
  6. </dependency>
  1. # application.yml
  2. spring:
  3. datasource:
  4. url: jdbc:mysql://localhost:3306/test?autoReconnect=true&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&useSSL=false
  5. username: root
  6. password: root
  7. driver-class-name: com.mysql.jdbc.Driver

然后配置测试类,唯一不同的是,在测试类上要多加一个@AutoConfigureTestDatabase注解:

  1. @RunWith(SpringRunner.class)
  2. @MybatisTest
  3. @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
  4. public class CityMapperTest2 {
  5. @SpringBootApplication(scanBasePackages = {"com.shouzheng.demo.mapper"})
  6. static class InnerConfig {}
  7. @Autowired
  8. private CityMapper cityMapper;
  9. // ...
  10. }

这样,测试的时候就会使用我们配置的数据库进行测试,而不是使用内存数据库。

事务回滚设置

测试持久层时,默认是回滚的。可以在具体的测试方法上添加@Rollback(false)来禁止回滚,也可以在测试类上添加。

集成测试

集成测试时会超脱一个类的范围,我们需要保证自测试目标类及以下的依赖类,都能够在spring容器中被实例化,最简单的方式莫过于构建完整的spring上下文。虽然这样一来,会有很多和测试目标无关的类也会被实例化,但是我们省去了精心设计初始化bean的工夫,而且也间接的达到了“测试构建完整的spring上下文”的目的。

从Controller开始测试

例如我们以上文中介绍到的controller为测试目标,测试newCity请求。测试类如下:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(classes = DemoTestSpringBootApplication.class)
  3. @AutoConfigureMockMvc
  4. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
  5. public class CityControllerWithRollbackTest {
  6. private static Logger LOG = LoggerFactory.getLogger(CityControllerWithRollbackTest.class);
  7. @Autowired
  8. private MockMvc mockMvc;
  9. @Before
  10. @After
  11. public void getAllCities() throws Exception {
  12. mockMvc.perform(MockMvcRequestBuilders.get("/cities"))
  13. .andDo(result -> {
  14. String content = result.getResponse().getContentAsString();
  15. LOG.info("cities = {}", content);
  16. });
  17. }
  18. @Test
  19. @Transactional
  20. // @Rollback(false)
  21. public void test1_insertCity() throws Exception {
  22. LOG.info("insert a city");
  23. mockMvc.perform(MockMvcRequestBuilders.post("/city")
  24. .contentType(MediaType.APPLICATION_JSON)
  25. .content("{\"name\": \"杭州\", \"state\": \"浙江\", \"country\": \"中国\"}"))
  26. .andExpect(MockMvcResultMatchers.status().isOk());
  27. }
  28. /**
  29. * 为了观察数据库是否回滚
  30. */
  31. @Test
  32. public void test2_doNothind() {
  33. }
  34. }

这段代码主要测试新增数据记录的请求,并在测试前后分别请求并打印当前的数据记录集。我们可以看到,在test1_insertCity方法运行之后打印的数据集,会比在此之前打印的数据集多一条记录,而这条记录正是我们申请新增的数据记录。

test2_doNothind是一个辅助的测试方法,在完成test1_insertCity方法之后,开始执行test2_doNothind测试。而测试前的打印数据记录集的行为,可以让我们观察到test1_insertCity测试中新增的数据是否发生回滚。

集成测试时使用@SpringBootTest注解,指定配置类为项目启动类。如果我们的项目是基于spring-cloud的微服务环境,那么也可以使用内部配置类来减少服务注册等相关的配置。

@AutoConfigureMockMvc是为了实例化MockMvc实例,用来发送http请求。

事务回滚设置

实验证明,集成测试依然可以支持数据库操作回滚,方案就是在测试方法上使用@Transactional注解,标识事务性操作。同时,我们依然可以使用@Rollback来设置是否回滚。

从中间层开始测试

集成测试不是非要从最顶层开始测试,我们也可以从service层开始测试:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest(classes = {DemoTestSpringBootApplication.class})
  3. @FixMethodOrder(MethodSorters.NAME_ASCENDING)
  4. public class CityServiceWithRollbackTest {
  5. private static Logger LOG = LoggerFactory.getLogger(CityServiceWithRollbackTest.class);
  6. @Autowired
  7. private CityService cityService;
  8. @Before
  9. @After
  10. public void printAllCities() {
  11. List<City> cities = cityService.getAllCities();
  12. LOG.info("{}", cities);
  13. }
  14. @Test
  15. @Transactional
  16. public void test1_insert() {
  17. City city = new City();
  18. city.setName("杭州");
  19. city.setState("浙江");
  20. city.setCountry("CN");
  21. cityService.save(city);
  22. LOG.info("insert a new city {}", city);
  23. }
  24. @Test
  25. public void test2_doNothind() {
  26. }
  27. }

这段代码的测试方案和上文的controller集成测试方案相同,都是测试新增操作,并在测试前后打印当前数据集,来演示是否支持事务回滚。

Mock

在spring项目的测试类中,我们可以对任意的类进行mock,如下面这样:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class CityServiceUnitTest {
  4. @MockBean
  5. private CityMapper cityMapper;
  6. ...
  7. }

定义一个field,对其添加@MockBean注解,就声明了对应类型的一个mock bean。如果spring上下文中已经存在对应类型的bean,将会被mock bean覆盖掉。

默认的情况下,mock bean的所有方法都是透明的:什么也不做,直接返回对应类型的默认值。声明返回引用类型的方法,将直接返回null;声明返回基本类型的方法,直接返回相应的默认值;声明无返回的方法,那更是透明的。

mock的作用对静态方法无效,静态方法会被实际调用。所以建议不要在静态方法中进行资源相关的处理,否则将无法进行模拟测试。比如,使用静态方法封装数据库操作的行为是不好的。

如上文所述,Mock的使用场景是我们只关注对应的方法是否执行了,而不关心实际的执行效果。实际代码中,我们可以按照下面的方式使用:

  1. @Test
  2. @Transactional
  3. public void test1_insert() {
  4. City city = new City();
  5. city.setName("杭州");
  6. city.setState("浙江");
  7. city.setCountry("CN");
  8. cityService.save(city);
  9. Mockito.verify(cityMapper).insert(city);
  10. LOG.info("insert a new city {}", city);
  11. }

Mockito.verify开始的一行,用来验证作为mock bean的cityMapper的insert方法会被执行,而且参数为city。如果方法没有被调用,或者实际调用时的传参不一致,都会导致测试失败。

比如,如果改成Mockito.verify(cityMapper).insert(new City());,将会抛出下面的异常:

  1. Argument(s) are different! Wanted:
  2. cityMapper bean.insert(null,null,null,null);
  3. -> at com.shouzheng.demo.web.CityServiceWithRollbackTest.test1_insert(CityServiceWithRollbackTest.java:56)
  4. Actual invocation has different arguments:
  5. cityMapper bean.insert(null,杭州,浙江,CN);
  6. -> at com.shouzheng.demo.web.CityService.save(CityService.java:26)
  7. Comparison Failure:
  8. Expected :cityMapper bean.insert(null,null,null,null);
  9. Actual :cityMapper bean.insert(null,杭州,浙江,CN);

Stub

在Mock的基础上更进一步,如果我们关注方法的返回结果,或者我们希望方法能有预定的行为,使得测试按照我们预期的方向进行,那么我们需要对mock bean的某些方法进行stub,让这些方法在参数满足某个条件的情况下,给我们预设的响应。

实际代码中,我们只能对mock bean的方法进行stub,否则得到下面的异常:

  1. org.mockito.exceptions.misusing.MissingMethodInvocationException:
  2. when() requires an argument which has to be 'a method call on a mock'.
  3. For example:
  4. when(mock.getArticles()).thenReturn(articles);
  5. Also, this error might show up because:
  6. 1. you stub either of: final/private/equals()/hashCode() methods.
  7. Those methods *cannot* be stubbed/verified.
  8. Mocking methods declared on non-public parent classes is not supported.
  9. 2. inside when() you don't call method on mock but on some other object.

返回预设的结果

我们可以按照下面的方式,让它返回预设的结果:

  1. Mockito.when(cityMapper.selectAllCities())
  2. .thenReturn(Collections.singletonList(city));

或者抛出预设的异常(如果我们检测异常处理代码的话):

  1. Mockito.when(cityMapper.selectAllCities())
  2. .thenThrow(new RuntimeException("test"));

或者去执行实际的方法:

  1. when(mock.someMethod()).thenCallRealMethod();

注意,调用真实的方法有违mock的本义,应该尽量避免。如果要调用的方法中调用了其他的依赖,需要自行注入其他的依赖,否则会空指针。

执行预设的操作

如果我们希望它能够执行预设的操作,比如打印我们传入的参数,或者修改我们传入的参数,我们可以按照下面的方式实现:

  1. Mockito.when(cityMapper.insert(Mockito.any()))
  2. .then(invocation -> {
  3. LOG.info("arguments are {}", invocation.getArguments());
  4. return 1;
  5. });

参数匹配

我们可以指定明确的参数匹配条件,或者使用模式匹配:

  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class MathServiceTest {
  4. @Configuration
  5. static class ConfigTest {}
  6. @MockBean
  7. private MathService mathService;
  8. @Test
  9. public void testDivide() {
  10. Mockito.when(mathService.divide(4, 2))
  11. .thenReturn(2);
  12. Mockito.when(mathService.divide(8, 2))
  13. .thenReturn(4);
  14. Mockito.when(mathService.divide(Mockito.anyInt(), Mockito.eq(0))) // 必须同时用模式
  15. .thenThrow(new RuntimeException("error"));
  16. Assertions.assertThat(mathService.divide(4, 2))
  17. .isEqualTo(2);
  18. Assertions.assertThat(mathService.divide(8, 2))
  19. .isEqualTo(4);
  20. Assertions.assertThatExceptionOfType(RuntimeException.class)
  21. .isThrownBy(() -> {
  22. mathService.divide(3, 0);
  23. })
  24. .withMessageContaining("error");
  25. }
  26. }

上面的测试可能有些奇怪,mock的对象也同时作为测试的目标。这是因为我们的目的在于介绍mock,所以简化了测试流程。

注意,如果我们对方法的其中一个参数使用了模式,其他的参数都需要使用模式。比如下面这句:

Mockito.when(mathService.divide(Mockito.anyInt(), Mockito.eq(0))),我们的本意是Mockito.when(mathService.divide(Mockito.anyInt(), 0)),但是我们不得不为第二个参数使用模式。

附录

相关注解的汇总

注解 说明
@RunWith junit的注解,通过这个注解使用SpringRunner.class,能够将junit和spring进行集成。后续的spring相关注解才会起效。
@SpringBootTest spring的注解,通过扫描应用程序中的配置来构建测试用的Spring上下文。
@AutoConfigureMockMvc spring的注解,能够自动配置MockMvc对象实例,用来在模拟测试环境中发送http请求。
@WebMvcTest spring的注解,切片测试的一种。使之替换@SpringBootTest能将构建bean的范围限定于web层,但是web层的下层依赖bean,需要通过mock来模拟。也可以通过参数指定只实例化web层的某一个到多个controller。具体可参考Auto-configured Spring MVC Tests
@RestClientTest spring的注解,切片测试的一种。如果应用程序作为客户端访问其他Rest服务,可以通过这个注解来测试客户端的功能。具体参考Auto-configured REST Clients
@MybatisTest mybatis按照spring的习惯开发的注解,切片测试的一种。使之替换@SpringBootTest,能够将构建bean的返回限定于mybatis-mapper层。具体可参考mybatis-spring-boot-test-autoconfigure
@JdbcTest spring的注解,切片测试的一种。如果应用程序中使用Jdbc作为持久层(spring的JdbcTemplate),那么可以使用该注解代替@SpringBootTest,限定bean的构建范围。官方参考资料有限,可自行网上查找资料。
@DataJpaTest spring的注解,切片测试的一种。如果使用Jpa作为持久层技术,可以使用这个注解,参考Auto-configured Data JPA Tests
@DataRedisTest spring的注解,切片测试的一种。具体内容参考Auto-configured Data Redis Tests

参考资料

基于spring-boot的应用程序的单元+集成测试方案的更多相关文章

  1. 基于Spring Boot、Spring Cloud、Docker的微服务系统架构实践

    由于最近公司业务需要,需要搭建基于Spring Cloud的微服务系统.遍访各大搜索引擎,发现国内资料少之又少,也难怪,国内Dubbo正统治着天下.但是,一个技术总有它的瓶颈,Dubbo也有它捉襟见肘 ...

  2. 基于Spring Boot/Spring Session/Redis的分布式Session共享解决方案

    分布式Web网站一般都会碰到集群session共享问题,之前也做过一些Spring3的项目,当时解决这个问题做过两种方案,一是利用nginx,session交给nginx控制,但是这个需要额外工作较多 ...

  3. step6----->往工程中添加spring boot项目------->修改pom.xml使得我的project是基于spring boot的,而非直接基于spring framework

    文章内容概述: spring项目组其实有多个projects,如spring IO platform用于管理external dependencies的版本,通过定义BOM(bill of mater ...

  4. 实战基于Spring Boot 2的WebFlux和mLab搭建反应式Web

    Spring Framework 5带来了新的Reactive Stack非阻塞式Web框架:Spring WebFlux.作为与Spring MVC并行使用的Web框架,Spring WebFlux ...

  5. 基于Spring Boot和Spring Cloud实现微服务架构学习

    转载自:http://blog.csdn.net/enweitech/article/details/52582918 看了几周Spring相关框架的书籍和官方demo,是时候开始总结下这中间的学习感 ...

  6. 基于Spring Boot和Spring Cloud实现微服务架构学习--转

    原文地址:http://blog.csdn.net/enweitech/article/details/52582918 看了几周spring相关框架的书籍和官方demo,是时候开始总结下这中间的学习 ...

  7. 基于Spring Boot和Spring Cloud实现微服务架构

    官网的技术导读真的描述的很详细,虽然对于我们看英文很费劲,但如果英文不是很差,请选择沉下心去读,你一定能收获好多.我的学习是先从Spring boot开始的,然后接触到微服务架构,当然,这一切最大的启 ...

  8. .NET CORE与Spring Boot编写控制台程序应有的优雅姿势

    本文分别说明.NET CORE与Spring Boot 编写控制台程序应有的“正确”方法,以便.NET程序员.JAVA程序员可以相互学习与加深了解,注意本文只介绍用法,不会刻意强调哪种语言或哪种框架写 ...

  9. 基于Spring Boot的统一异常处理设计

    基于Spring Boot的统一异常处理设计 作者: Grey 原文地址:https://www.cnblogs.com/greyzeng/p/11733327.html Spring Boot中,支 ...

随机推荐

  1. 阮一峰:自适应网页设计(Responsive Web Design)别名(响应式web设计)

    随着3G的普及,越来越多的人使用手机上网. 移动设备正超过桌面设备,成为访问互联网的最常见终端.于是,网页设计师不得不面对一个难题:如何才能在不同大小的设备上呈现同样的网页? 手机的屏幕比较小,宽度通 ...

  2. Ubuntu下快速部署安装 Nginx + PHP + MySQL 笔记

        先更新软件库 sudo apt-get update 安装 MySQL sudo apt-get install mysql-server 安装 Nginx sudo apt-get inst ...

  3. 纯javascript代码实现浏览器图片选择预览、旋转、批量上传

    工作中遇到的业务场景,和同事一起研究了下,主要是为了兼容IE版本 其实就是一些琐碎的知识点在网上搜集下解决方式,然后集成了下,主要有以下点: 1. IE input type=file的图片预览要用I ...

  4. 20155307 2016-2017-2 《Java程序设计》第6周学习总结

    20155307 2016-2017-2 <Java程序设计>第6周学习总结 教材学习内容总结 串流数据有来源及目的地,衔接两者的是串流对象.如果要将数据从来源取出,可以使用输入串流:如果 ...

  5. util.promisify 的那些事儿

    util.promisify是在node.js 8.x版本中新增的一个工具,用于将老式的Error first callback转换为Promise对象,让老项目改造变得更为轻松. 在官方推出这个工具 ...

  6. Linux/Unix系统编程手册 第一章:历史和标准

    Unix的开发不受控于某一个厂商或者组织,是由诸多商业和非商业团体共同贡献进行演化的.这导致两个结果:一是Unix集多种特性于一身,二是由于参与者众多,随着时间推移,Unix实现方式逐渐趋于分裂. 由 ...

  7. POI读取Excel数据保存到数据库,并反馈给用户处理信息(导入带模板的数据)

    今天遇到这么一个需求,将课程信息以Excel的形式导入数据库,并且课程编号再数据库中不能重复,也就是我们需要先读取Excel提取信息之后保存到数据库,并将处理的信息反馈给用户.于是想到了POI读取文件 ...

  8. mybatis查询参数为0时无法识别问题

    最近在工作中遇到一个mybatis参数问题,主要是列表查询按照状态进行过滤,其中已完成状态值是0,被退回是1.如图所示 , 然后Mapper里面是和平常一样的写法<if test="s ...

  9. 【洛谷题解】P2303 [SDOi2012]Longge的问题

    题目传送门:链接. 能自己推出正确的式子的感觉真的很好! 题意简述: 求\(\sum_{i=1}^{n}gcd(i,n)\).\(n\leq 2^{32}\). 题解: 我们开始化简式子: \(\su ...

  10. PHP 中 int 和 integer 类型的区别

    半夜整理东西,发现一个以前没留意到的小问题. function show($id) : int { return $id; } function show($id) : integer { retur ...