测试mock要优雅(转)
转自-谢恩德
作者github:https://github.com/DDGlove ##
小剧场:发现问题
小D :阿康,我最近需要给测试做一个东西:在测试过程中要跳过某些调用远程的方法,要怎么做啊? 阿康 :写一个不执行远程方法的接口不就好了吗?或者在代码里根据条件判断下要不要执行远程方法。 小D :还要写新接口或者改代码啊,一点都不优雅,还有更优雅的方法吗? 阿康 :那就多利用Spring和java代理做mock对象,我只能帮你到这里了。 小D :哎,别走啊,别走啊,展开说说展开说说
思考问题
什么是mock :作为一个动词,mock是模拟、模仿的意思;作为一个名词,mock是能够模仿真实对象行为的模拟对象。软件测试中,mock所模拟的对象一定不是我们所测试的对象,而是 SUT 的依赖(dependency)。换句话说,mock 的作用是模拟 SUT 依赖对象的行为。
测试的对象一般称之为SUT(Software Under Test)
参考:https://blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/114324301
mock的时候要注意什么:
- mock的对象要尽量准确:在A对象中使用了B,B对象使用了C,的情况,如果我们要对C跳过执行,那就最好对C进行mock,这样在测试过程中,我们依然还可以测试到B中的逻辑;如果直接mockB,可能就没覆盖到B的逻辑了。
- mock要尽量不要有大量的开发量:如果我们在使用mock的过程,因为mock而要多写很多代码,那就变得得不偿失了
- mock还需要考虑性能/可用性/易用性
解决问题
前话: @Resource注解注入对象时,先使用byName:找名字相同的对象,如果没有再使用byType:找类型相同的对象。
场景一:只在测试或开发环境使用mock
如:发短信(发短信太贵了,测试环境我们就不发短信了吧) ;远程服务不提供(我依赖的服务不提供测试环境给我,ToT)
思路:
在项目启动的时候,读取一个配置(可以是环境标识,也可以是一个独立的配置),根据不同的配置加载不同的实现类到对应的SpringContext里,调用方正常使用。
代码:
用短信服务举例:
//短信服务接口
package com.temp.project.server.demo;
public interface SmsService {
void sendSms(String phone);
boolean verifySms(String phone, String smsCode);
}
//短信服务真实实现
package com.temp.project.server.demo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
* 真实发短信服务
*/
@Service
@ConditionalOnProperty(name = "contract.autotest", havingValue = "false", matchIfMissing = true)
public class SmsServiceImpl implements SmsService{
@Override
public void sendSms(String phone) {
System.out.println("我去真实发短信了");
}
@Override
public boolean verifySms(String phone, String smsCode) {
System.out.println("我去真实验短信了");
return true;
}
}
//短信服务mock实现
package com.temp.project.server.demo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
* mock发短信服务
*/
@Service
@ConditionalOnProperty(name = "contract.autotest", havingValue = "true", matchIfMissing = false)
public class SmsServiceMockImpl implements SmsService{
@Override
public void sendSms(String phone) {
System.out.println("我不去真实发短信");
}
@Override
public boolean verifySms(String phone, String smsCode) {
System.out.println("我不去真实验短信");
return true;
}
}
//短信服务调用方实现
package com.temp.project.server.demo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserLoginServiceImpl {
@Resource
SmsService smsService;
public void beforeLogin(String userId) {
System.out.println("真实查询用户信息,获得手机号");
smsService.sendSms("用户手机号");
}
public boolean login(String userId, String smsCode) {
System.out.println("真实查询用户信息,获得手机号");
return smsService.verifySms("用户手机号", smsCode);
}
}
//测试方法
@Test
public void test() {
userService.beforeLogin("123");
userService.login("123","888888");
}
//添加了配置 contract.autotest = true 的测试结果:
真实查询用户信息,获得手机号
我不去真实发短信
真实查询用户信息,获得手机号
我不去真实验短信
//添加了配置 contract.autotest = false或者没有该配置 的测试结果:
真实查询用户信息,获得手机号
我去真实发短信了
真实查询用户信息,获得手机号
我去真实验短信了
优点:
非常优雅: 调用者完全不需要改动,被mock对象只做按需加载的改动。 非常高效:在服务加载期间按需加载了相应实现类,无额外的代码。
缺点:
不灵活: 不能动态切换走什么实现,如果需要切换需要修改配置和重启服务。
场景二:mock和真实的方法都需要保留,且服务类的入参都有公共参数。
如:生产某些用户绕过发短信(我们发短信服务所有接口入参刚好都设置了操作员参数,现在测试同学的手机号需要绕过短信验证的过程,比如需要自动化测试)
思路:
既然每个接口都有共同参数,那我们可以通过FactoryBean返回一个能根据不同的参数值转发到不同的实现类功能的代理类,而完成mock。
代码:
用短信服务举例
//短信服务接口 都有operator字段
package com.temp.project.server.demo;
public interface SmsService {
void sendSms(String operator, String phone);
boolean verifySms(String operator, String phone, String smsCode);
}
package com.temp.project.server.demo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
/**
* 真实发短信服务
*/
@Service
public class SmsServiceImpl implements SmsService{
@Override
public void sendSms(String operator, String phone) {
System.out.println("我去真实发短信了");
}
@Override
public boolean verifySms(String operator, String phone, String smsCode) {
System.out.println("我去真实验短信了");
return true;
}
}
package com.temp.project.server.demo;
import org.springframework.stereotype.Service;
/**
* mock发短信服务
*/
@Service
public class SmsServiceMockImpl implements SmsService{
@Override
public void sendSms(String operator, String phone) {
System.out.println("我是测试人员,我不去真实发短信");
}
@Override
public boolean verifySms(String operator, String phone, String smsCode) {
System.out.println("我是测试人员,我不去真实验短信");
return true;
}
}
//生成 可以自动需要实现类的 代理对象的 FactoryBean
package com.temp.project.server.demo;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@Primary
@Component
public class SmsServiceAutoFactory implements FactoryBean<SmsService>, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public SmsService getObject() throws Exception {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
InvocationHandler invocationHandler = new SmsServiceAutoFactory.SmsServiceInvocationHandler();
return (SmsService) Proxy.newProxyInstance(loader, new Class[]{SmsService.class}, invocationHandler);
}
@Override
public Class<?> getObjectType() {
return SmsService.class;
}
/**
* 根据是否需要mock,选择不同的处理类
*/
public class SmsServiceInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String beanName;
boolean canMock = args != null
&& args.length > 0
&& args[0] != null
&& args[0] instanceof String
&& args[0].equals("test");
if (canMock) {
beanName = SmsServiceMockImpl.class.getSimpleName();
//建议打日志方便追踪
} else {
beanName = SmsServiceImpl.class.getSimpleName();
}
beanName = StringUtils.substring(beanName, 0, 1).toLowerCase() +
StringUtils.substring(beanName, 1);
SmsService smsService = (SmsService) applicationContext.getBean(beanName);
return method.invoke(smsService, args);
}
}
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
//调用者
package com.temp.project.server.demo;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserLoginServiceImpl {
@Resource
SmsService smsService;
public void beforeLogin(String userId) {
System.out.println("真实查询用户信息,获得手机号");
smsService.sendSms(userId, "用户手机号");
}
public boolean login(String userId, String smsCode) {
System.out.println("真实查询用户信息,获得手机号");
return smsService.verifySms(userId, "用户手机号", smsCode);
}
}
//测试方法
@Test
public void test() {
userService.beforeLogin( "test");
userService.login("test","888888");
}
//test是测试人员
真实查询用户信息,获得手机号
我是测试人员,我不去真实发短信
真实查询用户信息,获得手机号
我是测试人员,我不去真实验短信
@Test
public void test() {
userService.beforeLogin( "user");
userService.login("user","888888");
}
真实查询用户信息,获得手机号
我去真实发短信了
真实查询用户信息,获得手机号
我去真实验短信了
优点:
非常优雅:调用者和真实对象完全不需要更改 非常灵活:只需要修改参数,就可以选择是否需要mock
缺点:
非常局限:需要接口所有方法都有相同的入参 比较高效:经过了一层代理,但是对性能影响微小
ps. 如果没有接口,而是一个类,可以写一个mock子类来继承该类,通过CGlib代理该类。 想要通过开发来控制是否走代理可以通过@ConditionalOnProperty来控制mock和factory是否加载
场景三:mock和真实的方法都需要保留,且接口没有公共参数
如我们的合同场景,在自动化测试需要mock,在正常情况保证走正常逻辑,且合同没有公共参数。
思路:
我们可以通过ThreadLocal在入口传一个值,在实现类的代理类上取到这个值做判断使用哪个实现类。
代码:
代码过多不贴了,详细可以看: project项目(feature/auto-test 分支)中: com.temp.project.server.config.WebMockConfig —-mock前端参数拦截 com.temp.project.common.util.MockUtil ——-mock工具类 com.temp.project.common.util.TTLUtil ——-ThreadLocal工具类 com.temp.project.contract.impl.ContractSPIAutoFactory —–cglib代理类生成FactoryBean com.temp.project.contract.impl.ContractMockSPI —-合同mock处理类
优点:
非常优雅:所有参数的拦截都放在了拦截类里面,入口无需改动。实现类改动甚少。 可扩展性高:抽取了mock工具类和mock拦截器,需要要新增别的mock,会比较方便。
缺点:
相对高效:一些不需要mock的api接口也会走到拦截类里判断一次,对性能影响微小。 相对局限:这类方式只适合前端到后端,可以随意加个header。不适合dubbo,虽然从mockUtil角度来说,是不局限的,但是使用起来不够优雅了。
总结:
我们不管是在开发过程中,或者是项目上线以后,都有需要对某个对象进行模拟的情况,利用好Spring IOC、DI和JAVA的动态代理、CGlib动态代理,就能实现优雅的mock对象。
才疏学浅,欢迎大家指出错误。也欢迎大家提出自己的想法、意见和建议,内容有你才会变得更丰富完整。 感谢@郑康(zhengkang-qmlua)和@刘远兴(liuyuanxing-ou99b)对本文的贡献。
╰(*°▽°*)╯ (dadaguo-dadaguo)