簡體   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