这里只是了解一下jmockit的基本原理,不涉及太多细节。
基本上来说,jmockit的工作原理是通过asm修改原有class的字节码,然后通过jdk的instrument机制替换现有class的内容,达到mock方法或者属性的目的。在测试完成后再恢复原有class。
先看一个简单的使用例子:
public class JmockitTest1 { @Test public void testMockUp() { new MockUp<Foo>() { @Mock public void say() { System.out.println("I'm the mock one"); } }; Foo foo = new Foo(); System.out.print("testMockUp:"); foo.say(); } @Test public void testReal() { Foo foo = new Foo(); System.out.print("testReal:"); foo.say(); } } class Foo { public void say() { System.out.println("I'm the real one"); } }
打印出的内容是:
testMockUp:I'm the mock one testReal:I'm the real one
以上代码的核心在于new MockUp的时候,会调用
Mockit.setUpMock(classToMock, this);
这个方法里面根据注解取得所有需要mock的方法
ublic static void setUpMock(Class<?> realClass, Object mock) { Class<?> mockClass = mock.getClass(); new RedefinitionEngine(realClass, mock, mockClass).redefineMethods(); }
然后生成新的字节码modifiedClassFile,并调用redefineMethods
private void redefineMethods(boolean forStartupMock) { if (mockMethods.getMethodCount() > 0 || mockingConfiguration != null) { byte[] modifiedClassFile = modifyRealClass(forStartupMock); redefineMethods(mockMethods.getMockClassInternalName(), modifiedClassFile, !forStartupMock); } }
redefineMethods方法做两件事情:
1. 修改方法
2. 注册,以供方法结束时改回来
public void redefineMethods(String mockClassInternalName, byte[] modifiedClassfile, boolean register) { redefineMethods(modifiedClassfile); if (register) { addToMapOfRedefinedClasses(mockClassInternalName, modifiedClassfile); } }
其中,修改方法中本质上调用的是
private void redefineMethods(byte[] modifiedClassfile) { redefineMethods(new ClassDefinition(realClass, modifiedClassfile)); } public static void redefineMethods(ClassDefinition... classDefs) { try { Startup.instrumentation().redefineClasses(classDefs); } ... }
这里就是利用了jdk的Instrumentation.redefineClasses细节可以参考其javadoc,意思就是修改了当前加载的这个类的方法定义(不过以及在执行中的方法不受影响).所以它可以如此神奇的本质原因还是jdk提供了修改class的接口。
之前因为注册了修改过的类,所以在BlockJUnit4ClassRunnerDecorator.executeTest结束时会调用
finally { TestRun.resetExpectationsOnAnnotatedMocks(); savePoint.rollback(); TestRun.exitNoMockingZone(); } public void rollback() { restoreClassesTransformedAfterSavepoint(); restoreClassesRedefinedAfterSavepoint(); TestRun.getMockClasses().getRegularMocks().removeInstances(previousMockInstancesCount); }
restoreClassesRedefinedAfterSavepoint这根据类名从文件中重新加载类的byte[],并修改回去
public void restoreOriginalDefinition(Class<?> aClass) { realClass = aClass; byte[] realClassFile = new ClassFile(aClass, false).getBytecode(); redefineMethods(realClassFile); }
事实上jmockit通过junit会调用:
package org.junit.runner; public abstract class Runner implements Describable { static { if ("1.6 1.7".contains(System.getProperty("java.specification.version"))) { mockit.internal.startup.Startup.initializeIfNeeded(); } } } 实际调用: public void initializeAccordingToJDKVersion() { String jarFilePath = discoverPathToJarFile(); if (Startup.jdk6OrLater) { new JDK6AgentLoader(jarFilePath).loadAgent(); } else if ("1.5".equals(Startup.javaSpecVersion)) { throw new IllegalStateException( "JMockit has not been initialized. Check that your Java 5 VM has been started " + "with the -javaagent:" + jarFilePath + " command line option."); } else { throw new IllegalStateException("JMockit requires a Java 5 VM or later."); } }
testng也是类似的初始化方法。其实整个代码可读性非常好,这里的解释语句都有点多余。