繁体   English   中英

如何使用 java 代理和 ASM 监控抽象 class 中方法的调用?

[英]How to monitor the invocation of methods in abstract class using java agent and ASM?

我要做的是监控JUnit 4种测试方法的调用。 我必须自己这样做的原因是:我需要记录每个测试方法执行过程中执行的类。 所以我需要在测试方法中插入一些指令,以便我知道测试何时开始/结束以及那些记录的类是由哪个测试实体执行的。 所以我需要自己过滤测试方法。

实际上,我这样做的原因与问题无关,因为我没有在标题中提到“JUnit”、“test”。 在其他类似情况下,该问题仍然可能是一个问题。

我的情况是这样的:

public abstract class BaseTest {
    @Test
    public void t8() {
        assert new C().m() == 1;
    }
}
public class TestC  extends BaseTest{
    // empty
}

我还修改了 Surefire 的成员argLine ,以便在 Surefire 启动新的 JVM 进程以执行测试时附加我的代理(预主模式)。

在我的代理 class 中:

    public static void premain(String args, Instrumentation inst){
        isPreMain = true;
        agentArgs = args;
        log("args: " + args);
        parseArgs(args);
        inst.addTransformer(new TestTransformer(), true);
    }

我的变压器 class:

public class TestTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {

        log("TestTransformer: transform: " + className);
        ...
        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
        RecordClassAdapter mca = new RecordClassAdapter(cw, className);
        cr.accept(mca, 0);
        return cw.toByteArray();
    }
}

在我的 ClassVisitor 适配器 class 中:

class RecordClassAdapter extends ClassVisitor {
    ...
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        mv = new RecordMethodAdapter (...);
        return mv;
    }
}

在我的 MethodVisitor 适配器 class 中:

class RecordMethodAdapter extends MethodVisitor {
    public void visitCode() {
        mv.visitCode();
        if (isTestMethod){
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(INVOKESTATIC, MyClass, "entityStarted",
                    "(Ljava/lang/String;)V", false);
        }
    }
}

可悲的是,我发现抽象 class 不会进入transform方法,因此我无法检测t8方法。 TestC应该作为测试 class 执行,但我永远无法监视TestC.t8的调用。

有几个机会通过 JUnit API 将日志记录注入测试。 不需要仪器。

对于一个非常简单的设置:

public class BaseTest {
    @Test
    public void t8() {
        System.out.println("Running  test "+getClass().getName()+".t8() [BaseTest.t8()]");
    }
  
    @Test
    public void anotherMethod() {
        System.out.println("Running  test "
            +getClass().getName()+".anotherMethod() [BaseTest.anotherMethod()]");
    }
}

public class TestC extends BaseTest {
    @Rule
    public TestName name = new TestName();
  
    @Before
    public void logStart() throws Exception {
       System.out.println("Starting test "+getClass().getName()+'.'+name.getMethodName());
    }
  
    @After
    public void logEnd() throws Exception {
       System.out.println("Finished test "+getClass().getName()+'.'+name.getMethodName());
    }
}

这将打印

Starting test class TestC.t8
Running  test TestC.t8() [BaseTest.t8()]
Finished test class TestC.t8
Starting test class TestC.anotherMethod
Running  test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test class TestC.anotherMethod

您也可以实施自己的规则。 例如特设:

public class TestB extends BaseTest {
    @Rule
    public TestRule notify = TestB::decorateTest;
  
    static Statement decorateTest(Statement st, Description d) {
        return new Statement() {
            @Override public void evaluate() throws Throwable {
              System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
              st.evaluate();
              System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
            }
        };
    }
}

或者作为可以通过单线插入测试 class 的可重用规则

public class LoggingRule implements TestRule {
    public static final LoggingRule INSTANCE = new LoggingRule();
  
    private LoggingRule() {}
  
    @Override
    public Statement apply(Statement base, Description description) {
        Logger log = Logger.getLogger(description.getClassName());
        log.setLevel(Level.FINEST);
        Logger.getLogger("").getHandlers()[0].setLevel(Level.FINEST);
        String clName = description.getClassName(), mName = description.getMethodName();
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                log.entering(clName, mName);
                String result = "SUCCESS";
                try {
                    base.evaluate();
                }
                catch(Throwable t) {
                    result = "FAIL";
                    log.throwing(clName, mName, t);
                }
                finally {
                    log.exiting(clName, mName, result);
                }
            }
        };
    }
}

使用起来很简单

public class TestB extends BaseTest {
    @Rule
    public LoggingRule log = LoggingRule.INSTANCE;
}

另一种方法是实现自定义测试运行器。 这允许将行为应用于整个测试套件,因为测试套件也是通过运行器实现的。

public class LoggingSuiteRunner extends Suite {
    public LoggingSuiteRunner(Class<?> klass, RunnerBuilder builder)
                                                            throws InitializationError {
        super(klass, builder);
    }

    @Override
    public void run(RunNotifier notifier) {
        notifier.addListener(LOG_LISTENER);
        try {
            super.run(notifier);
        } finally {
            notifier.removeListener(LOG_LISTENER);
        }
    }

    static final RunListener LOG_LISTENER = new RunListener() {
        public void testStarted(Description d) {
            System.out.println("Starting test "+d.getClassName()+"."+d.getMethodName());
        }
        public void testFinished(Description d) {
            System.out.println("Finished test "+d.getClassName()+"."+d.getMethodName());
        }
        public void testFailure(Failure f) {
            Description d = f.getDescription();
            System.out.println("Failed test "+d.getClassName()+"."+d.getMethodName()
                              +": "+f.getMessage());
        };
    };
}

这可能会应用于整个测试套件,即仍然从BaseTest继承测试方法,您可以使用

@RunWith(LoggingSuiteRunner.class)
@SuiteClasses({ TestB.class, TestC.class })
public class TestA {}

public class TestB extends BaseTest {}

public class TestC extends BaseTest {}

这将打印

Starting test TestB.t8
Running  test TestB.t8() [BaseTest.t8()]
Finished test TestB.t8
Starting test TestB.anotherMethod
Running  test TestB.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestB.anotherMethod
Starting test TestC.t8
Running  test TestC.t8() [BaseTest.t8()]
Finished test TestC.t8
Starting test TestC.anotherMethod
Running  test TestC.anotherMethod() [BaseTest.anotherMethod()]
Finished test TestC.anotherMethod

这些只是建议,建议研究 API,它允许更多。 要考虑的另一点是,根据您用于启动测试的方法(您提到了 maven 插件),可能支持在此处添加全局RunListener ,而无需更改测试类。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM