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



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

事实上,我这样做的原因与问题无关,因为我没有提到";JUnit"测试";在标题中。在其他类似的情况下,这个问题仍然可能是一个问题。

我的案例是这样的:

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

我还修改了Surefire的成员argLine,这样当Surefire启动一个新的JVM进程来执行测试时,我的代理将被附加(premain模式(。

在我的代理类:

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

我的变压器类别:

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 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 RecordMethodAdapter extends MethodVisitor {
public void visitCode() {
mv.visitCode();
if (isTestMethod){
mv.visitLdcInsn(methodName);
mv.visitMethodInsn(INVOKESTATIC, MyClass, "entityStarted",
"(Ljava/lang/String;)V", false);
}
}
}

遗憾的是,我发现抽象类不会进入transform方法,因此我无法为t8方法提供工具。TestC应该作为一个测试类来执行,但我永远无法监视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

您也可以执行自己的规则。例如,ad-hoc:

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());
}
};
}
}

或者作为一个可重复使用的规则,可以通过单个内衬插入测试类

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,而无需更改测试类。

最新更新