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

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

@RunWith(SpringRunner.class) @SpringBootTest(classes = {DemoTestSpringBootApplication.class}) @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CityServiceWithRollbackTest { private static Logger LOG = LoggerFactory.getLogger(CityServiceWithRollbackTest.class); @Autowired private CityService cityService; @Before @After public void printAllCities() { List<City> cities = cityService.getAllCities(); LOG.info("{}", cities); } @Test @Transactional public void test1_insert() { City city = new City(); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); cityService.save(city); LOG.info("insert a new city {}", city); } @Test public void test2_doNothind() { } }

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

Mock

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

@RunWith(SpringRunner.class) @SpringBootTest public class CityServiceUnitTest { @MockBean private CityMapper cityMapper; ... }

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

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

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

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

@Test @Transactional public void test1_insert() { City city = new City(); city.setName("杭州"); city.setState("浙江"); city.setCountry("CN"); cityService.save(city); Mockito.verify(cityMapper).insert(city); LOG.info("insert a new city {}", city); }

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

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

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

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

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

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

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

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

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

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

或者去执行实际的方法:

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

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

执行预设的操作

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

Mockito.when(cityMapper.insert(Mockito.any())) .then(invocation -> { LOG.info("arguments are {}", invocation.getArguments()); return 1; }); 参数匹配

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

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