一、Mock框架:Mockito
- Mockito既可以mock类或接口的全部方法,也可以只mock部分方法并让其它方法使用真实对象的方法,同时还可以验证mock方法的调用情况。
API文档:javadoc.io/doc/org.moc…
Tutorial:javadoc.io/doc/org.moc… dzone.com/refcardz/mo…
引入Maven依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
</dependency>
创建mock对象:
RedirectVerifyCache redirectVerifyCache = Mockito.mock(RedirectVerifyCache.class);
Mockito既可以mock类也可以mock接口;
mock对象所有方法的默认行为是返回该方法返回类型对应的默认值,可以通过打桩来改变;
mock对象一旦创建,对这个对象的所有操作都会被记录,可以用来验证方法的调用情况。
创建spy对象:
RedirectVerifyCache realObject = new RedirectVerifyCache();
RedirectVerifyCache spyObject = Mockito.spy(realObject);
Mockito支持spy对象,只对部分方法打桩,让其它未被打桩的方法调用真实对象的方法;
Mockito在创建spy对象的时候,实际上是对真实对象进行了一次拷贝。
2、打桩(stubbing):定义mock方法的行为
支持两种打桩方式:“when.. thenXXX”和“doXXX..when”
Mockito.when(redirectVerifyCache.isValidSrcPkg(Matchers.anyString())).thenReturn(true);Mockito.doNothing().when(redirectVerifyCache).afterPropertiesSet();
(1)when.. thenXXX
Mockito.when()方法接收mock方法的返回值,这里传入对mock方法的调用;
Mockito.when()方法返回一个OngoingStubbing对象,这里调用它的thenXXX方法来定义mock方法的行为;
thenXXX支持如下方法:
Mockito会根据when方法入参的类型来对thenXXX的方法值做编译期类型检查,保证类型的一致性。所以,这种方式适合用来对返回值非void的方法打桩;
返回值为void的方式无法不能使用when.. thenXXX,只能用doXXX..when。
(2)doXXX..when
Mockito.doXXX方法返回一个Stubber对象;
Stubber对象的when方法接收mock对象,并返回mock对象;
最后再调用mock对象中被打桩的方法;
其中,doXXX所支持的方法与thenXXX类似:
doXXX..when可以用来打桩返回值为void的mock方法;
对Spy对象中方法的打桩建议用doXXX..when方式,因为when.. thenXXX方式会在实际打桩之前调用mock方法,而spy对象中方法在打桩之前的默认行为是调用真实对象的方法——触发了一次真实调用。
(3)“when.. thenXXX”和“doXXX..when”中mock方法的参数匹配
默认情况下,对mock方法参数的匹配使用对象的equals方法,只能匹配一个值;
Mockito对匹配一组值提供了多种支持,如anyInt(), anyThat(), isA()等:
查阅更多,点击这里和这里。
(4)自定义Answer
“when.. thenXXX”和“doXXX..when”分别提供了thenAnswer(Answer answer)和doAnswer(Answer answer)方法,可以用来自定义返回值,通常比较复杂的测试用例才会用到;
when(appConfig.getIntProperty(anyString(), anyInt())).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
Map<String, Integer> map = new HashMap<>();
map.put("intent.default.ability", 1);
map.put("intent.black.ability", 1);
map.put("ext.verify.s1", 0);
map.put("ext.verify.s2", 1);
return map.get(invocation.getArgumentAt(0, String.class));
}
});
在自定义的Answer中,可以访问mock方法被调用时的入参。
3.验证(Verifying):验证mock方法的调用情况
Mockito除了提供打桩能力,还提供了验证mock方法调用情况的能力(Mockito会记录mock对象的所有操作)。如果mock方法的调用情况不符合预期,当前测试用例就会被标记为失败。
验证mock方法被调用的次数:
RedirectVerifyCache redirectVerifyCache = Mockito.mock(RedirectVerifyCache.class);
Mockito.when(redirectVerifyCache.isValidSrcPkg(Matchers.anyString())).thenReturn(true);
redirectVerifyCache.isValidSrcPkg("some.src.pkg.name");
Mockito.verify(redirectVerifyCache, Mockito.times(1)).isValidSrcPkg(anyString());
Mockito支持多种指定次数的方式:
除了验证mock方法被调用的次数外,Mockito还支持:
验证mock方法的执行顺序;
验证mock方法被调用时所传的参数值;
验证方法执行时间不超过指定值。
4.Mockito的局限性
Mockito基于动态代理,使用继承的方式来生成代理对象,不支持mock下面这些元素:
Final类、枚举、final方法、静态方法、私有方法、hashCode方法和equals方法。
Mockito最新版本正在尝试提供某些特性,如mock final类和final方法,不过目前还只是试验版本。查阅详细,点击这里。
如果在写测试用例时遇到这些问题,可以尝试重构业务代码,让业务代码更加可测。如果确实必要,可以使用PowerMock框架。
二、Mockito框架的补充:PowerMock
PowerMock基于字节码操纵和自定义类加载器,在测试用例执行的时候运行时修改字节码,主要解决一些用常规mock框架无法处理的测试问题。通常建议慎用powermock框架。
PowerMock支持mock静态方法、构造器、final类、final方法和私有方法;支持移除静态初始化块;支持访问私有属性,获取对象的中间状态等。
API文档:
Tutorial:github.com/powermock/p…
引入Maven依赖:
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.7.1</version>
<scope>test</scope>
</dependency>
在测试类加上@RunWith(PowerMockRunner.class) 注解和 @PrepareForTest注解:
@RunWith(PowerMockRunner.class)
@PrepareForTest(ConfigService.class) //mock方法所在的类
public class TokenServiceTest {
}
Pwoermock提供了PowerMockito工具类来创建mock类,而打桩和验证仍然可以使用Mockito提供的静态方法:
PowerMockito.mockStatic(ConfigService.class);
Mockito.when(ConfigService.getAppConfig()).thenReturn(appConfig);
查阅更多用法,点击Tutorial。
三、如何处理与RPC服务有关的单元测试?
参考《202005 朱檐 单元测试》
我们的项目中有些地方对RPC的调用是以异步调用的方式进行的,最终从RPC上下文获取调用结果。
用when.. thenAnswer的方式mock RPC服务:
Mockito.when(appServiceV2.queryDetailForChangePkg(Mockito.anyString(), Mockito.any())).thenAnswer(new AppDetailByPkgAnswer());
自定义Answer时,把返回值也放一份到RPC上下文中:
public class AppDetailByPkgAnswer implements Answer<AppDetail> {
@Override
public AppDetail answer(InvocationOnMock invocation) throws Throwable {
AppDetail appDetail = null;
String pkgName = invocation.getArgumentAt(0, String.class);
if (AppDetailAnswer.APP_DETAIL_9223372036854775806.getPkgName().equals(pkgName)) {
appDetail = AppDetailAnswer.APP_DETAIL_9223372036854775806;
}
FutureAdapter<Object> futureAdapter = new FutureAdapter<>(new MockResponseFuture<>(appDetail));
RpcContext.getContext().setFuture(futureAdapter);
return appDetail;
}
}
四、Spring对单元测试的支持
- 我们的项目都是建立在Spring的基础上,代码风格受Spring影响,比如@Autowired字段注入和IoC容器依赖等。Spring本身也提供了对单元测试和集成测试的支持,提供了一些非常实用的工具。
官网:
引入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>1.5.3.RELEASE</version>
<scope>test</scope>
</dependency>
spring-boot-starter-test已经包含了Mockito等框架:
- 通用的测试工具类
Org.springframework.test.util包下提供了一组通用的测试工具类,如反射工具类ReflectionTestUtils和APO工具类AopTestUtils等;
ReflectionTestUtils支持修改常量值、读写非public字段和调用非public方法等,可以用来处理被测对象的@Autowired私有字段注入、私有生命周期方法等依赖问题。
@Component
public class TokenService {
@Autowired
private RedirectVerifyCache redirectVerifyCache;
@Autowired
private CpdInfoData cpdInfoData;
RedirectVerifyCache redirectVerifyCache = Mockito.mock(RedirectVerifyCache.class);
when(redirectVerifyCache.isValidSrcPkg(anyString())).thenReturn(true);
ReflectionTestUtils.setField(tokenService, "redirectVerifyCache", redirectVerifyCache, RedirectVerifyCache.class);
3、为测试用例提供Spring容器支持:Spring TestContext Framework.
集成测试以及有些单元测试需要启动Spring容器,从而让测试对象和测试方法能够访问Spring容器中的bean。
(1)Spring TestContext Framework的使用
在测试类上添加如下2个注解:
@RunWith(SpringRunner.class)
@SpringBootTest(classes=Main.class)
public abstract class IntegrationTestBase {
}
可以通过@SpringBootTest注解的classes参数指定Spirng容器的启动配置,这里指定为跟业务代码一样的配置。如果classes参数缺省,将默认按照如下规则搜索配置:
从包含该测试类的包开始向上搜索,直到找到一个由@SpringBootApplication或@SpringBootConfiguration注解的类,使用该类的配置。
在测试类中使用Spring容器的依赖注入,获取容器中的bean
@RunWith(SpringRunner.class)
@SpringBootTest(classes=Main.class)
public abstract class IntegrationTestBase {
@Autowired
private RedirectVerifyCache redirectVerifyCache;
@MockBean
private AppServiceV2 appServiceV2;
其中,@MockBean注解创建mock对象并用该mock对象替换Spring容器中所有同类型的bean。可以在测试用例运行之前用Mockito框架对mock对象打桩。
(2)测试对象/测试方法与Spring TestContext
1)如果一个测试类中有多个测试方法,JUnit会为每个测试方法各创建一个测试对象;
2)Spring TestContext不参与测试对象的创建,测试对象也不会被放入Spring TestContext;
测试对象中的依赖注入是由DependencyInjectionTestExecutionListener完成的。
(3)Spring TestContext的缓存机制
1)从理论上来说,各个测试方法彼此独立,Spring TestContext Framework会为每个测试方法各提供一个Spring TestContext;
2)由于Spring TestContext的启动是一个非常耗时的过程,Spring TestContext Framework会缓存已启动的Spring TestContext,缓存所用的key是用于加载它的配置参数组合(包括xml文件或配置class等);
3)如果两个测试方法在同一个JVM运行并且使用相同的配置启动容器,Spring TestContext Framework事实上给它们提供的是同一个Spring TestContext;
4)可以在测试类或测试方法上加上@DirtiesContext注解,让Spring移除缓存中的Spring TestContext并在运行下一个测试用例时重新启动一个Spring TestContext。
五、如何处理与数据库有关的单元测试?
- 建议使用内存数据库,一方面让单元测试不受外界环境影响,另一方面避免单元测试污染真实数据库(尤其是现在开发环境直接连测试环境数据库)。
Spring提供了对内存数据库的支持,原生支持HSQL、H2和Derby等3种类型的内存数据库。
引入Maven依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<scope>test</scope>
</dependency>
- 获取数据源
以H2数据库为例,引入Maven依赖:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
方式1:XML方式
<jdbc:embedded-database id="dataSource" generate-name="true">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
其中,schema.sql和test-data.sql分别是用于建表和初始化数据库的SQL脚本;此时一个类型为javax.sql.DataSource的bean将被注入到Spring容器中。
或者用下面这种方式:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd">
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.h2.Driver"/>
<property name="url" value="jdbc:h2:mem:appstore_resource;mode=mysql;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1"/>
</bean>
<jdbc:initialize-database>
<jdbc:script location="classpath:mappers/resource/schema.sql"/>
</jdbc:initialize-database>
</beans>
“mode=mysql”指定H2数据库以MySQL模式启动,避免产生SQL语法兼容问题。
方式2:编程方式
EmbeddedDatabase db = new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(H2)
.setScriptEncoding("UTF-8")
.ignoreFailedDrops(true)
.addScript("schema.sql")
.addScripts("user_data.sql", "country_data.sql")
.build();
// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource)
db.shutdown()
这种方式不依赖于Spring容器,其中EmbeddedDatabase为javax.sql.DataSource的子类。
- 执行SQL语句/脚本
方式1:借助直接操作数据库的工具类
1)org.springframework.test.jdbc包下的JdbcTestUtils提供了一些直接进行数据库操作的静态方法:
countRowsInTable(..): 查询指定表中的记录条数;
countRowsInTableWhere(..): 查询指定表中符合where条件的记录条数;
deleteFromTables(..): 删除指定表中的所有记录;
deleteFromTableWhere(..): 删除指定表中符合where条件的记录条数;
dropTables(..): 删除指定的表。
例如:
int result = intentTokenMapper.insertIntentToken(intentToken);
int row = JdbcTestUtils.countRowsInTableWhere(new JdbcTemplate(dataSource), "intent_token", "valid_state=1");
2)执行SQL脚本的工具类
org.springframework.jdbc.datasource.init.ScriptUtils
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests
org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests
这些工具类都需要传入一个JdbcTemplate类型的参数,用于指定数据源。这种方式不依赖于Spring容器。
方式2:使用Spring容器中的@sql注解
@Test
@Sql(scripts = {"classpath:mappers/resource/insert_test_intent_key.sql"}, executionPhase = BEFORE_TEST_METHOD)
@Sql(scripts = {"classpath:mappers/resource/cleanup.sql"}, executionPhase = AFTER_TEST_METHOD)
public void testQueryKey() {
List<IntentKeyEntity> intentKeyEntities = intentTokenMapper.queryKey(0, 10);
assertEquals(1, intentKeyEntities.size());
assertEquals(1, intentKeyEntities.get(0).getType().intValue());
assertEquals("c97a9e97ddd4096c", intentKeyEntities.get(0).getPrivateKey());
}
@sql注解可以指定执行SQL语句或SQL脚本,默认使用Spring容器中的数据源,还需要指定在测试方法之前还是之后执行相应的SQL脚本。