git:
https://github.com/alibaba/testable-mock
文档:
https://alibaba.github.io/testable-mock/#/
单测工具于我而言的作用,就是可以对关键节点有测试,保证长期开发或重构的稳定和正确性;
操作上越简单易用越好,越节省开发时间越好;TestableMock是个较好的选择;
TestableMock是利用字节码增强技术,来进行Mock的工具; 阅读上述文档基本可以做到快速上手;它主要优点:
- 访问被测类私有成员:使单元测试能直接调用和访问被测类的私有成员,解决私有成员初始化和私有方法测试的问题
- 快速Mock任意调用:使被测类的任意方法调用快速替换为Mock方法,实现"指哪换哪",解决传统Mock工具使用繁琐的问题
- 辅助测试void方法:利用Mock校验器对方法的内部逻辑进行检查,解决无返回值方法难以实施单元测试的问题
TestableMock简化设计主要基于两条基本假设:
假设一:同一个测试类里,一个测试用例里需要Mock掉的方法,在其他测试用例里通常也都需要Mock。因为这些被Mock的方法往往访问了不便于测试的外部依赖。
假设二:需要Mock的调用都来自被测类的代码。此假设是符合单元测试初衷的,即单元测试只应该关注当前单元的内部行为,单元外的逻辑应该被替换为Mock。
源码中有demo模块,其中使用方式还是很详细的;这里简单列举下常用方法,感兴趣可以用起来:
- 来测些调用外部RPC接口的方法;
RPC接口
public interface WeatherApi {
@RequestLine("GET /api/weather/city/{city_code}")
WeatherExample.Response query(@Param("city_code") String cityCode);
}
被测类
public class CityWeather {
private static final String API_URL = "http://t.weather.itboy.net";
private static final String BEI_JING = "101010100";
private static final String SHANG_HAI = "101020100";
private static final String HE_FEI = "101220101";
public static final Map<String, String> CITY_CODE = MapUtil.builder(new HashMap<String, String>())
.put(BEI_JING, "北京市")
.put(SHANG_HAI, "上海市")
.put(HE_FEI, "合肥市")
.build();
private static WeatherApi weatherApi = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(WeatherApi.class, API_URL);
public String queryShangHaiWeather() {
WeatherExample.Response response = weatherApi.query(SHANG_HAI);
return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
}
private String queryHeFeiWeather() {
WeatherExample.Response response = weatherApi.query(HE_FEI);
return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
}
public static String queryBeiJingWeather() {
WeatherExample.Response response = weatherApi.query(BEI_JING);
return response.getCityInfo().getCity() + ": " + response.getData().getYesterday().getNotice();
}
public static void main(String[] args) {
CityWeather cityWeather = new CityWeather();
String shanghai = cityWeather.queryShangHaiWeather();
String hefei = cityWeather.queryHeFeiWeather();
String beijing = CityWeather.queryBeiJingWeather();
System.out.println(shanghai);
System.out.println(hefei);
System.out.println(beijing);
}
测试类
@EnablePrivateAccess
public class CityWeatherTest {
@TestableMock(targetMethod = "query")
public WeatherExample.Response query(WeatherApi self, String cityCode) {
WeatherExample.Response response = new WeatherExample.Response();
// mock天气接口调用返回的结果
response.setCityInfo(new WeatherExample.CityInfo().setCity(
CityWeather.CITY_CODE.getOrDefault(cityCode, cityCode)));
response.setData(new WeatherExample.Data().setYesterday(
new WeatherExample.Forecast().setNotice("this is from mock")));
return response;
}
CityWeather cityWeather = new CityWeather();
/**
* 测试 public方法调用
*/
@Test
public void test_public() {
String shanghai = cityWeather.queryShangHaiWeather();
System.out.println(shanghai);
assertEquals("上海市: this is from mock", shanghai);
}
/**
* 测试 private方法调用
*/
@Test
public void test_private() {
String hefei = (String) PrivateAccessor.invoke(cityWeather, "queryHeFeiWeather");
System.out.println(hefei);
assertEquals("合肥市: this is from mock", hefei);
}
/**
* 测试 静态方法调用
*/
@Test
public void test_static() {
String beijing = CityWeather.queryBeiJingWeather();
System.out.println(beijing);
assertEquals("北京市: this is from mock", beijing);
}
}
- 调用外部方法的void方法
例如,下面这个方法会根据输入打印信息到控制台:
class Demo {
public void recordAction(Action action) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss ");
String timeStamp = df.format(new Date());
System.out.println(timeStamp + "[" + action.getType() + "] " + action.getTarget());
}
}
若要测试此方法,可以利用TestableMock快速Mock掉System.out.println方法。在Mock方法体里可以继续执行原调用(相当于并不影响本来方法功能,仅用于做调用记录),也可以直接留空(相当于去除了原方法的副作用)。
在执行完被测的void类型方法以后,用InvokeVerifier.verify()校验传入的打印内容是否符合预期:
class DemoTest {
private Demo demo = new Demo();
// 拦截`System.out.println`调用
@MockMethod
public void println(PrintStream ps, String msg) {
// 执行原调用
ps.println(msg);
}
@Test
public void testRecordAction() {
Action action = new Action("click", ":download");
demo.recordAction();
// 验证Mock方法`println`被调用,且传入参数符合预期
verify("println").with(matches("\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \[click\] :download"));
}
}
3.还有些好用的功能例如,识别当前测试用例和调用来源
在Mock方法中通过TestableTool.SOURCE_METHOD变量可以识别进入该Mock方法前的被测类方法名称;此外,还可以借助TestableTool.MOCK_CONTEXT变量为Mock方法注入“额外的上下文参数”,从而区分处理不同的调用场景。
例如,在测试用例中验证当被Mock方法返回不同结果时,对被测目标方法的影响:
@Test
public void testDemo() {
MOCK_CONTEXT.set("case", "data-ready");
assertEquals(true, demo());
MOCK_CONTEXT.set("case", "has-error");
assertEquals(false, demo());
MOCK_CONTEXT.clear();
}
在Mock方法中取出注入的参数,根据情况返回不同结果:
@MockMethod
private Data mockDemo() {
switch((String)MOCK_CONTEXT.get("case")) {
case "data-ready":
return new Data();
case "has-error":
throw new NetworkException();
default:
return null;
}
}
注意,由于TestableMock并不依赖(也不希望依赖)任何特定测试框架,因而无法自动识别单个测试用例的结束位置,这使得设置到TestableTool.MOCK_CONTEXT变量的参数可能会在同测试类中跨测试用例存在。建议总是在使用后及时使用MOCK_CONTEXT.clear()清空上下文
在当前版本中,此变量在运行期的效果类似于一个在测试类中的普通Map类型成员对象,但请尽量使用此变量而非自定义对象传递附加的Mock参数,以便在将来升级至v0.5版本时获得更好的兼容性。
TestableTool.MOCK_CONTEXT变量的值是在测试类内共享的,当单元测试并行运行时,建议请选择parallel类型为classes
完整代码示例见java-demo和kotlin-demo示例项目中的should_able_to_get_source_method_name()和should_able_to_get_test_case_name()测试用例。