Jmockit使用详解之Faking
简介
Faking主要用来构造假的实现,通常是部分伪造方法和构造函数,来到得到期望的运行逻辑,常用来伪造第三方组件(发邮件,发请求等)。
伪造方法和类
方法通过@Mock来伪造,被伪造的类只要继承mockit.MockUp< T >就行,这里T是需要被伪造的类
public final class FakeLoginContext extends MockUp<LoginContext>
{
//伪造了LoginContext的部分方法和构造函数
//伪造了构造函数
@Mock
public void $init(String name, CallbackHandler callback) {
assertEquals("test", name);
assertNotNull(callback);
}
@Mock
public void login() {}
@Mock
public Subject getSubject() { return null; }
}
当一个类的方法被伪造后,在实际调用过程中,真实类的这些方法将会被替换成伪造类的方法。
注意@Mock标注的方法要和实际该类(或父类,不包括Object类)中的方法签名匹配:对于普通方法,需要名称和参数一致,对于构造函数,只需要构造函数参数一致,同时名字用”$init”。需要注意的是,构造函数的mock实际是要基于原有构造函数能够成功运行,init的方法中的内容只不过在原有构造函数后构造后多加了一些内容,因此常配合Invocation来使用,这样可以获取构造后的实例,并做一些改动。
如果@Mock的方法在实际类中找不到,将会抛IllegalArgumentException异常。
使用伪造类
伪造类可以作用于整个test类,或者一个test方法,@BeforeClass,@BeforeMethod,@Before,@BeforeEach也可以使用
@Test
public void applyingAFakeClass() throws Exception {
//直接新建了整个fake类,它就可以作用整个方法
new FakeLoginContext());
// Inside an application class which creates a suitable CallbackHandler:
new LoginContext("test", callbackHandler).login();
...
}
Faking的伪造能力
private,protect,后者包private的类,静态方法,final方法,native方法都可以伪造。甚至 一个静态方法能够被实例方法伪造(伪造类中定义的非静态方法和静态放的签名相同)。需要注意的是抽象方法不能够被伪造。
伪造不确定的类
public interface Service { int doSomething(); }
final class ServiceImpl implements Service { public int doSomething() { return 1; } }
public final class TestedUnit
{
//类实现了接口
private final Service service1 = new ServiceImpl();
//这里使用了匿名内部类,是无法访问的
private final Service service2 = new Service() { public int doSomething() { return 2; } };
public int businessOperation() {
return service1.doSomething() + service2.doSomething();
}
}
给定一个基类,我们可以伪造其所有的实现类(不仅是已经存在,也包括后序创建的)
public <T extends Service> void fakingImplementationClassesFromAGivenBaseType() {
new MockUp<T>() {
@Mock int doSomething() { return 7; }
};
int result = new TestedUnit().businessOperation();
assertEquals(14, result);
}
伪造类初始化
下面的方法可以伪造静态初始化代码块,也包括静态属性:
注意:运行时才有效的静态属性的赋值将会无效(jvm只初始化一次类)
@Test
public void fakingStaticInitializers() {
new MockUp<ClassWithStaticInitializers>() {
@Mock
void $clinit() {
// Do nothing here (usually).
}
};
ClassWithStaticInitializers.doSomething();
}
访问调用上下文:
伪造的方法可以有一个多余的Invocation参数,并且作为第一个参数,这个参数在调用的时候将会自动传入。调用的上下文将可以通过该方法获取。比如getInvokedInstance()将会访返回伪造的实例,如果返回null,表明这个方法是静态的。
@Test
public void accessingTheFakedInstanceInFakeMethods() throws Exception
{
Subject testSubject = new Subject();
new MockUp<LoginContext>() {
@Mock
void $init(Invocation invocation, String name, Subject subject) {
assertNotNull(name);
assertSame(testSubject, subject);
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
// Verifies that this is the first invocation.
// 获取伪造类的调用次数
assertEquals(1, invocation.getInvocationCount());
// Forces setting of private Subject field, since no setter is available.
Deencapsulation.setField(loginContext, subject);
}
@Mock
void login(Invocation invocation) {
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
// getSubject() returns null until the subject is authenticated.
assertNull(loginContext.getSubject());
// Private field set to true when login succeeds.
Deencapsulation.setField(loginContext, "loginSucceeded", true);
}
@Mock
void logout(Invocation invocation) {
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
assertSame(testSubject, loginContext.getSubject());
}
};
LoginContext theFakedInstance = new LoginContext("test", testSubject);
theFakedInstance.login();
theFakedInstance.logout();
}
伪造类中继续调用真实类的方法
@Test
public void proceedIntoRealImplementationsOfFakedMethods() throws Exception {
// Create objects used by the code under test:
LoginContext loginContext = new LoginContext("test", null, null, configuration);
// Apply fakes:
ProceedingFakeLoginContext fakeInstance = new ProceedingFakeLoginContext();
// Exercise the code under test:
assertNull(loginContext.getSubject());
loginContext.login();
assertNotNull(loginContext.getSubject());
assertTrue(fakeInstance.loggedIn);
fakeInstance.ignoreLogout = true;
loginContext.logout(); // first entry: do nothing
assertTrue(fakeInstance.loggedIn);
fakeInstance.ignoreLogout = false;
loginContext.logout(); // second entry: execute real implementation
assertFalse(fakeInstance.loggedIn);
}
static final class ProceedingFakeLoginContext extends MockUp<LoginContext>
{
boolean ignoreLogout;
boolean loggedIn;
@Mock
void login(Invocation inv) throws LoginException {
try {
//执行真实的方法
inv.proceed(); // executes the real code of the faked method
loggedIn = true;
}
finally {
// This is here just to show that arbitrary actions can be taken inside the
// fake, before and/or after the real method gets executed.
LoginContext lc = inv.getInvokedInstance();
System.out.println("Login attempted for " + lc.getSubject());
}
}
@Mock
void logout(Invocation inv) throws LoginException {
// We can choose to proceed into the real implementation or not.
if (!ignoreLogout) {
inv.proceed();
loggedIn = false;
}
}
}
伪造的重复使用
在Junit中可以使会用@Before注解
public class MyTestClass
{
@BeforeClass
public static void applySharedFakes() {
new MockUp<LoginContext>() {
// shared @Mock's here...
};
}
// test methods that will share the fakes applied above...
}
全局伪造
使用JUnit的Suite方法,在MyFirstTest.class, MySecondTest.class将会有效
@RunWith(Suite.class)
@Suite.SuiteClasses({MyFirstTest.class, MySecondTest.class})
public final class TestSuite
{
@BeforeClass
public static void applyGlobalFake() {
new FakeLogging();
}
}
添加AOP的advice
public final class MethodTiming extends MockUp<Object>
{
private final Map<Method, Long> methodTimes = new HashMap<>();
public MethodTiming(Class<?> targetClass) { super(targetClass); }
MethodTiming(String className) throws ClassNotFoundException { super(Class.forName(className)); }
//这里并不是Mock,是AOP的advice,写法是固定的
//用来统计所有方法的调用时间
@Mock
public Object $advice(Invocation invocation) {
long timeBefore = System.nanoTime();
try {
return invocation.proceed();
}
finally {
long timeAfter = System.nanoTime();
long dt = timeAfter - timeBefore;
Method executedMethod = invocation.getInvokedMember();
Long dtUntilLastExecution = methodTimes.get(executedMethod);
Long dtUntilNow = dtUntilLastExecution == null ? dt : dtUntilLastExecution + dt;
methodTimes.put(executedMethod, dtUntilNow);
}
}
@Override
protected void onTearDown() {
System.out.println("\nTotal timings for methods in " + targetType + " (ms)");
for (Entry<Method, Long> methodAndTime : methodTimes.entrySet()) {
Method method = methodAndTime.getKey();
long dtNanos = methodAndTime.getValue();
long dtMillis = dtNanos / 1000000L;
System.out.println("\t" + method + " = " + dtMillis);
}
}
}