简体   繁体   中英

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

What I want to do is to monitor the invocation of JUnit 4 test methods. The reason I must do this by myself is: I need to record the executed classes during the execution of each test method. So I need to insert some instructions to the test method so that I know when the test start/end and those recorded classes are executed by which test entity. So I need to filter the test methods on my own.

Actually, the reason why I am doing this is not relevant to the question , as I did not mention "JUnit", "test" in the title. The problem can still be a problem in other similar cases.

The case I have is like this:

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

I have also modified Surefire's member argLine so that my agent will be attached (premain mode) when Surefire launch a new JVM process to execute tests.

In my agent class:

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

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

In my ClassVisitor adapter 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;
    }
}

In my MethodVisitor adapter 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);
        }
    }
}

Sadly, I found that the abstract class will not get into the transform method, thus I can not instrument the t8 method. TestC should be executed as a test class, but I can never monitor the invocation of TestC.t8 .

There are several opportunities to inject logging into the test via the JUnit API. There is no need for instrumentation.

For a very simple setup:

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

which will print

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

You can also implement your own rule. Eg 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());
            }
        };
    }
}

Or as a reusable rule that can be inserted via a single-liner into a test 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);
                }
            }
        };
    }
}

used as simple as

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

A different approach is implementing a custom test runner. This allows to apply a behavior to an entire test suite, as test suites are implemented via runners as well.

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

This may get applied to an entire test suite, ie still inheriting test methods from BaseTest , you may use

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

public class TestB extends BaseTest {}

public class TestC extends BaseTest {}

which will print

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

These are only pointers, to suggest studying the API which allows even more. Another point to consider, is that depending on the method you're using for launching the tests (you mentioned a maven plugin), there might be support for adding a global RunListener right there, without the need to alter the test classes.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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