Testable-Mock增强之路

最近我们又在热火朝天的搞起来了单元测试。但谈到单元测试,总是不可避免的涉及到Mock,如何方便地Mock,是一个很大的难题。

之所以会是这样的原因,主要是因为Mock的时候希望尽量不侵入原有代码,而且可以任意对部分,包括私有方法进行Mock。

在比较工具之后,我选择了最近火热的testable-mock框架,但是在使用的过程发现如果我想进行流程测试,并不是一个测试类对应一个被测类,这样的方式去Mock,有没有办法去增强一下呢? 我们定义一个全局的类,将我们依赖RPC Mock进去,并且支持数据模板的配置?

抱着这个想法,我开始了自己的折腾(zuosi)路线。

Testable-Mock的原理

开始之前,我们简单了解一下字节码增强的原理。

在java 1.5开始, 开始有了一个魔法技能,就是在JVM加载某个CLASS允许修改之后再进行加载。这个神奇的功能就是java.lang.instrument, 核心的接口是ClassFileTransformer

有了这个之后,好多字节码操作框架都涌现出来,比如ASM,javassist框架,有的工具甚至让我们不用了解字节码的结构,

就可以很方便的去修改我们想要修改的类。

Testable-Mock正是基于这个机制进行的实现,操作字节码也是通过ASM框架。

TestableClassTransformer作为class文件修改的入口,在程序启动的时候会加载agent的com.alibaba.testable.agent.PreMain#premain加载这个transformer。

Testable-Mock的设计

明白了Testable-Mock的设计之后,让我们来看看它的设计吧。

在改进的过程中我最大的感觉是它的设计比较绕,一开始没有把握它的模型关系,导致走了很多弯路。

入口逻辑:

我们从com.alibaba.testable.agent.transformer.TestableClassTransformer#transform开始。

if (mockClassParser.isMockClass(cn)) {
		// it's a mock class
		bytes = new MockClassHandler(className).getBytes(bytes);
		BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
		return bytes;
}
String mockClass = foundMockForSourceClass(className);
if (mockClass != null) {
  // it's a source class with testable enabled
  List<MethodInfo> injectMethods = mockClassParser.getTestableMockMethods(mockClass);
  bytes = new SourceClassHandler(injectMethods, mockClass).getBytes(bytes);
  BytecodeUtil.dumpByte(cn, GlobalConfig.getDumpPath(), bytes);
  return bytes;
}
  1. 它会对每一个Class会进行判断,是否是MockClass,那么使用MockClassHandler进行处理。

  2. 不是MockClass的话,那么就按SourceClass进行处理,寻找是否有对应的MockClass

  3. 如果可以找到MockClass,那么就对SourceClass中的进行替换

入口逻辑相当的简单,也很容易明白。我们接着向下看。

MockClassHandler:

@Override
protected void transform(ClassNode cn) {
    LogUtil.diagnose("Found mock class %s", cn.name);
    if (!CLASS_OBJECT.equals(cn.superName)) {
        MockAssociationUtil.recordSubMockContainer(ClassUtil.toDotSeparatedName(cn.superName),
            ClassUtil.toDotSeparatedName(cn.name));
    }
    injectRefFieldAndGetInstanceMethod(cn);
    int mockMethodCount = 0;
    for (MethodNode mn : cn.methods) {
        if (isMockMethod(mn)) {
            mockMethodCount++;
            mn.access = BytecodeUtil.toPublicAccess(mn.access);
            // firstly, unfold target class from annotation to parameter
            unfoldTargetClass(mn);
            // secondly, add invoke recorder at the beginning of mock method
            injectInvokeRecorder(mn);
            // thirdly, add association checker before invoke recorder
            injectAssociationChecker(mn);
            // finally, handle testable util variables
            handleTestableUtil(mn);
        }
    }
    LogUtil.diagnose("  Found %d mock methods", mockMethodCount);
}

逻辑处理过程:

  1. 首先创建一个Public Static的singleton变量到这个类中,这样在其他类中来引用Mock方法的时候就不需要new了。

    private void injectRefFieldAndGetInstanceMethod(ClassNode cn) {
            String byteCodeMockClassName = ClassUtil.toByteCodeClassName(mockClassName);
            MethodNode getInstanceMethod = new MethodNode(ACC_PUBLIC | ACC_STATIC, GET_TESTABLE_REF,
                VOID_ARGS + byteCodeMockClassName, null, null);
            ...
    }
    
  2. 找到这个MockClass的所有MethodNode,循环处理。根据注释我们也很容易明白

  3. 首先,会把这个方法设置成public的。

  4. 解析target class参数,下面的步骤是处理记录、association类型的,我们这里就不展开了。

这里的核心就是增加了一个静态的GET_TESTABLE_REF方法和将target class放入到每一个mock method的第一个参数里。

foundMockForSourceClass:

private String foundMockForSourceClass(String className) {
    String mockClass = lookForMockWithAnnotationAsSourceClass(className);
    if (mockClass != null) {
        return mockClass;
    }
    mockClass = foundMockForTestClass(ClassUtil.getTestClassName(className));
    if (mockClass != null) {
        return mockClass;
    }
    return foundMockForInnerSourceClass(className);
}

找MockClass的方式有三个:

  1. @MockWith的方式去查找。如果SourceClass包含这个@MockWith注解,那么就查看对应的class。

  2. 按TestClass去寻找。主要的逻辑是假如是测试类,按@MockWith方式去寻找。如果没有找到,就按照className的方式去寻找,逻辑是将Test关键字替换成Mock看是否可以找到。

    private String foundMockForTestClass(String className) {
        ClassNode cn = adaptInnerClass(ClassUtil.getClassNode(className));
        if (cn != null) {
            String mockClass = lookForMockWithAnnotationAsTestClass(cn);
            if (mockClass != null) {
                return mockClass;
            }
            mockClass = lookForInnerMockClass(cn);
            if (mockClass != null) {
                return mockClass;
            }
        }
        return lookForOuterMockClass(className);
    }
    
  3. 在SourceClass内部中寻找。

SourceClassHandler:

我们重点关注一下com.alibaba.testable.agent.handler.SourceClassHandler#transformMethod方法

do {
    // 如果是我们要操作的instructions类型
    if (invokeOps.contains(instructions[i].getOpcode())) {
        MethodInsnNode node = (MethodInsnNode)instructions[i];
        if (CONSTRUCTOR.equals(node.name)) {
            ...
        } else {
            ...
            // 找到可以替换的Mock方法
            MethodInfo mockMethod = getMemberInjectMethodName(memberInjectMethods, node);
            if (mockMethod != null) {
                // it's a member or static method and an inject method for it exist
                int rangeStart = getMemberMethodStart(instructions, i);
                if (rangeStart >= 0) {
                    if (rangeStart < i) {
                        handleFrameStackChange(mn, mockMethod, rangeStart, i);
                    }
                    // 替换
                    instructions = replaceMemberCallOps(mn, mockMethod,
                        instructions, node.owner, node.getOpcode(), rangeStart, i);
                    i = rangeStart;
                } else {
                    LogUtil.warn("Potential missed mocking at %s:%s", mn.name, getLineNum(instructions, i));
                }
            }
        }
    }
    i++;
} while (i < instructions.length);

这里有两个点我们要关注一下:

  1. invokeOps.contains(instructions[i].getOpcode())的invokeOps,包括4种类型,只有这四种类型的我们才需要替换。从下面的代码,我们可以知道主要是对方法的调用进行替换。

     private final Set<Integer> invokeOps = new HashSet<Integer>() {{
            add(Opcodes.INVOKEVIRTUAL);
            add(Opcodes.INVOKESPECIAL);
            add(Opcodes.INVOKESTATIC);
            add(Opcodes.INVOKEINTERFACE);
     }};
    
  2. getMemberInjectMethodName(memberInjectMethods, node);根据node从MockClass的memberInjectMethods中获取可以使用的MockMethod。

看到这里,它的整体设计我们已经了解了。接下来我们总结一下。

总结:

模型关系总结

一个测试类对应一个被测类,被测类会引用好几个引用类,我们的Mock类定义的targetClass和targetMethod应该指向引用类的class和method。这点我在开始的时候总是以为对应的是被测类。

流程总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNB5LOk8-1632972595544)(https://raw.githubusercontent.com/Sutonline/md-img-bed/master/testable-mock%E5%8E%9F%E6%9C%89%E6%B5%8B%E8%AF%95%E6%B5%81%E7%A8%8B.jpg)]

明白了它的设计之后,那么我们来开始搞起来!

Testable-Mock改进

改进的方式很简单,主要有两个点。

  1. 解析我们定义的GlobalMockClass,存储MockClass和TargetClass、TargetMethod的关系
  2. 在寻找MockClass的时候做一些改动,如果包括我们全局的Global中的TargetClass就可以进行替换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFIJGvNj-1632972595547)(https://raw.githubusercontent.com/Sutonline/md-img-bed/master/Testable%E6%94%B9%E8%BF%9B%E6%B5%81%E7%A8%8B.png)]

类设计方面也比较简单:

  1. 继承原有的MockClassParser,存储全局的targetClass-method信息
  2. 继承原有的SourceClassHandler,从单个MockClass替换成Method方法集合
  3. 实现DataReader,可以基于文件配置读取方法入参返回结果

更具体的细节可以参见代码: 代码地址

总结

初次的字节码增强旅程并不是很顺利,只是迈出了一小步,不过感觉的确很神奇。对于ASM以及类加载的过程都没有系统的去理解,这个层次的领域知识还需要继续学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值