In the JMockit toolkit, the Faking API provides support for the creation of fake implementations. Typically, a fake targets a few methods and/or constructors in the class to be faked, while leaving most other methods and constructors unmodified.
Fake implementations can be particularly useful in tests which depend on external components or resources such as e-mail or web services servers, complex libraries, etc. Often, the fakes will be applied from reusable testing infrastructure components, not directly from a test class.
The replacement of real implementations with fake ones is completely transparent to the code which uses those dependencies, and can be switched on and off for the scope of a single test, all tests in a single test class, or for the entire test run.
In the context of the Faking API, a fake method is any method in a fake class that gets annotated with
@Mock
.
A fake class is any class extending the mockit.MockUp<T>
generic base class, where T
is the type
to be faked.
The example below shows several fake methods defined in a fake class for our example "real" class,
javax.security.auth.login.LoginContext.
public final class FakeLoginContext extends MockUp<LoginContext> {
@Mock
public void $init(String name, CallbackHandler callback) {
assertEquals("test", name);
assertNotNull(callback);
}
@Mock
public void login() {}
@Mock
public Subject getSubject() { return null; }
}
When a fake class is applied to a real class, the latter gets the implementation of those methods and constructors which have corresponding fake methods temporarily replaced with the implementations of the matching fake methods, as defined in the fake class. In other words, the real class becomes "faked" for the duration of the test which applied the fake class. Its methods will respond accordingly whenever they receive invocations during test execution. At runtime, what really happens is that the execution of a faked method/constructor is intercepted and redirected to the corresponding fake method, which then executes and returns (unless an exception/error is thrown) to the original caller, without this one noticing that a different method was actually executed. Normally, the "caller" class is one being tested, while the faked class is a dependency.
Each @Mock
method must have a corresponding "real method/constructor" with the same signature in
the targeted real class.
For a method, the signature consists of the method name and parameters; for a constructor, it's just the parameters,
with the fake method having the special name "$init
".
Finally, notice there is no need to have fake methods for all methods and constructors in a real class. Any such method or constructor for which no corresponding fake method exists in the fake class will simply stay "as is", that is, it won't be faked.
A given fake class must be applied to a corresponding real class to have any effect.
This is usually done for a whole test class or test suite, but can also be done for an individual test.
Fakes can be applied from anywhere inside a test class: a @BeforeClass
method, a
@BeforeMethod
/ @Before
/ @BeforeEach
method (TestNG / JUnit 4 / JUnit 5), or from a @Test
method.
Once a fake class is applied, all executions of the faked methods and constructors of the real class get automatically redirected to the
corresponding fake methods.
To apply the FakeLoginContext
fake class above, we simply instantiate it:
@Test
public void applyingAFakeClass() throws Exception {
new FakeLoginContext());
// Inside an application class which creates a suitable CallbackHandler:
new LoginContext("test", callbackHandler).login();
...
}
Since the fake class is applied inside a test method, the faking of LoginContext
by
FakeLoginContext
will be in effect only for that particular test.
When the constructor invocation that instantiates LoginContext
executes, the corresponding "$init
"
fake method in FakeLoginContext
will be executed.
Similarly, when the LoginContext#login
method is called, the corresponding fake method will be executed, which in this case
will do nothing since the method has no parameters and void
return type.
The fake class instance on which these invocations occur is the one created in the first part of the test.
So far, we have only faked public instance methods with public instance fake methods.
Several other kinds of methods in a real class can also be faked: methods with protected
or "package-private" accessibility,
static
methods, final
methods, and native
methods.
Even more, a static
method in the real class can be faked by an instance fake method, and vice-versa (an instance
real method with a static
fake).
Methods to be faked need to have an implementation, though not necessarily in bytecode (in the case of native
methods).
Therefore, an abstract
method cannot be faked directly.
Note that fake methods don't need to be public
.
To demonstrate this feature, lets consider the following code under test.
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();
}
}
The method we want to test, businessOperation()
, uses classes that implement a separate interface,
Service
.
One of these implementations is defined through an anonymous inner class, which is completely inaccessible (except for the use of
Reflection) from client code.
Given a base type (be it an interface
, an abstract
class, or any sort of base class), we can write a test which
only knows about the base type but where all implementing/extending implementation classes get faked.
To do so, we create a fake whose target type refers only to the known base type, and does so through a type variable.
Not only will implementation classes already loaded by the JVM get faked, but also any additional classes that happen to get loaded by
the JVM during later test execution.
This ability is demonstrated below.
@Test
public <T extends Service> void fakingImplementationClassesFromAGivenBaseType() {
new MockUp<T>() {
@Mock int doSomething() { return 7; }
};
int result = new TestedUnit().businessOperation();
assertEquals(14, result);
}
In the test above, all invocations to methods implementing Service#doSomething()
will be redirected to the fake method
implementation, regardless of the actual class implementing the interface method.
When a class performs some work in one or more static initialization blocks, we may need to stub it out so it doesn't interfere with test execution. We can define a special fake method for that, as shown below.
@Test
public void fakingStaticInitializers() {
new MockUp<ClassWithStaticInitializers>() {
@Mock
void $clinit() {
// Do nothing here (usually).
}
};
ClassWithStaticInitializers.doSomething();
}
Special care must be taken when the static initialization code of a class is faked.
Note that this includes not only any "static
" blocks in the class, but also any assignments to static
fields
(excluding those resolved at compile time, which do not produce executable bytecode).
Since the JVM only attempts to initialize a class once, restoring the static initialization code of a faked class will have no
effect.
So, if you fake away the static initialization of a class that hasn't been initialized by the JVM yet, the original class initialization
code will never be executed in the test run.
This will cause any static fields that are assigned with expressions computed at runtime to instead remain initialized with the
default values for their types.
A fake method can optionally declare an extra parameter of type mockit.Invocation
, provided it is the
first parameter.
For each actual invocation to the corresponding faked method/constructor, an Invocation
object will be
automatically passed in when the fake method is executed.
This invocation context object provides several getters which can be used inside the fake method.
One is the getInvokedInstance()
method, which returns the faked instance on which the invocation occurred
(null
if the faked method is static
).
Other getters provide the number of invocations (including the current one) to the faked method/constructor, the invocation arguments
(if any), and the invoked member (a java.lang.reflect.Method
or
java.lang.reflect.Constructor
object, as appropriate).
Below we have an example test.
@Test
public void accessingTheFakedInstanceInFakeMethods() throws Exception {
new MockUp<LoginContext>() {
Subject testSubject;
@Mock
void $init(Invocation invocation, String name, Subject subject) {
assertNotNull(name);
assertNotNull(subject);
// Verifies this is the first invocation.
assertEquals(1, invocation.getInvocationCount());
}
@Mock
void login(Invocation invocation) {
// Gets the invoked instance.
LoginContext loginContext = invocation.getInvokedInstance();
assertNull(loginContext.getSubject()); // null until subject is authenticated
testSubject = new Subject();
}
@Mock
void logout() { testSubject = null; }
@Mock
Subject getSubject() { return testSubject; }
};
LoginContext theFakedInstance = new LoginContext("test", new Subject());
theFakedInstance.login();
assertSame(testSubject, theFakedInstance.getSubject();
theFakedInstance.logout();
assertNull(theFakedInstance.getSubject();
}
Once a @Mock
method is executing, any additional calls to the corresponding faked method are also
redirected to the fake method, causing its implementation to be re-entered.
If, however, we want to execute the real implementation of the faked method, we can call the proceed()
method on the
Invocation
object received as the first parameter to the fake method.
The example test below exercises a LoginContext
object created normally (without any faking in effect at
creation time), using an unspecified configuration
.
@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;
}
}
}
In the example above, all the code inside the tested LoginContext
class will get executed, even though
some methods (login
and logout
) are faked.
This example is contrived; in practice, the ability to proceed into real implementations would not normally be useful for
testing per se, not directly at least.
You may have noticed that use of Invocation#proceed(...)
in a fake method effectively behaves like advice (from AOP
jargon) for the corresponding real method.
This is a powerful ability that can be useful for certain things (think of an interceptor or decorator).
Often, a fake class needs to be used throughout multiple tests, or even applied for the test run as a whole.
One option is to use test setup methods that run before each test method; with JUnit, we use the
@Before
annotation; with TestNG, it's @BeforeMethod
.
Another is to apply fakes inside of a test class setup method: @BeforeClass
.
Either way, the fake class is applied by simply instantiating it inside the setup method.
Once applied, a fake will remain in effect for the execution of all tests in the test class.
The scope of a fake applied in a "before" method includes the code in any "after" methods the test class may have (annotated with
@After
for JUnit or @AfterMethod
for TestNG).
The same goes for any fakes applied in a @BeforeClass
method: they will still be in effect during the
execution of any AfterClass
methods.
Once the last "after" or "after class" method finish being executed, though, all fakes get automatically "torn down".
For example, if we wanted to fake the LoginContext
class with a fake class for a bunch of related tests, we
would have the following methods in a JUnit test class:
public class MyTestClass {
@BeforeClass
public static void applySharedFakes() {
new MockUp<LoginContext>() {
// shared @Mock's here...
};
}
// test methods that will share the fakes applied above...
}
It is also possible to extend from a base test class, which may optionally define "before" methods that apply one or more fakes.
Sometimes, we may need to apply a fake for the entire scope of a test suite (all of its test classes), ie, a "global" fake. This can be done through external configuration, by setting a system property.
The fakes
system property supports a comma-separated list of fully qualified fake class names.
If specified at JVM startup time, any such class (which must extend MockUp<T>
) will be automatically applied
for the whole test run.
The fake methods defined in startup fake classes will remain in effect until the end of the test run, for all test classes.
Each fake class will be instantiated through its no-args constructor, unless an additional value was provided after the class name (for
example, as in "-Dfakes=my.fakes.MyFake=anArbitraryStringWithoutCommas
"), in which case the fake class should have a
constructor with one parameter of type String
.
There is one more special @Mock
method that can appear in a fake class: the "$advice
"
method.
If defined, this fake method will handle executions of each and every method in the target class (or classes, when applying the fake over
unspecified classes from a base type).
Differently from regular fake methods, this one needs to have a particular signature and return type:
Object $advice(Invocation)
.
For demonstration, lets say we want to measure the execution times of all methods in a given class during test execution, while still executing the original code of each method.
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
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);
}
}
}
The fake class above can be applied inside a test, in a "before" method, in a "before class" method, or even for the entire test run by
setting "-Dfakes=testUtils.MethodTiming=my.application.AppClass
".
It will add up the execution times for all executions of all methods in a given class.
As shown in the implementation of the $advice
method, it can obtain the java.lang.reflect.Method
that is being
executed.
If desired, the current invocation count and/or the invocation arguments could be obtained through similar calls to the
Invocation
object.
When the fake is (automatically) torn down, the onTearDown()
method gets executed, dumping measured timings to standard
output.