Unraveling MockK’s black magic
This article is an English version of the blog. All credits belong to oboenikui.
MockK is a mocking library written in Kotlin and for Kotlin. In addition to the traditional functionality provided by JVM based mocking libraries, Kotlin’s language feature significantly improves MockK’s usability.
However, aside from MockK, have you ever wondered how a JVM based mocking library works under the hood? Moreover, when using other libraries together with a mocking library, have you ever had trouble in identifying the root cause encountered? These questions might explain why some may shy away from using MockK, or other mocking libraries. To solve such problems, we need to investigate the details underneath.
In this article, we are going to look under the hood: In MockK, what does the
mockk method actually do? How do
verify set up the mock and check the number of calls?
This article does not intend to explain how to use MockK. Although MockK is made to work on Android and JS, the explanation will focus on JVM (Hotspot).
Note: For the rest of this article, MockK refers to the library, while
mockk refers to the method.
Create mocks using `mockk`
What’s the class of mock instance?
The type used to mock affects the class of the created mock instance. The answer might be different based on whether we are mocking a regular class, an interface, or an abstract class. Let’s try with the following code:
We will get the following output:
For a regular class, whether qualified with
final or not, the class of the created instance is the given class itself. In the case of an abstract class or an interface, an instance of the subclass is created. It is a well-known fact that it is impossible to instantiate an interface or abstract class immediately and we can only instantiate by implementing or inheriting a concrete class instead. Such limitation applies to mock instance.
Where do the names,
Interface$Subclass1, come from exactly? Furthermore, without generating subclasses, how do regular classes allow customization such as stubbing the return value of a specific method?
ByteBuddy: rewriting class and generating subclass
To answer the question above, we need to touch base on the library ByteBuddy. It is a library that provides the ability to dynamically create and modifying Java classes. Although it internally manipulates Java bytecode, the provided API is so convenient that it does not require knowledge of Java bytecode.
In MockK, the mocked class and all its superclass are rewritten using ByteBuddy in order to intercept the method call. Check out the implementation InliningClassTransformer.kt.
If we specify the following test VM option, the
.class files of the generated subclass will be saved in the specified location:
systemProperty 'io.mockk.classdump.path', 'output'
Let’s try with the above option and confirm that the class is rewritten when a regular class is mocked.
class transform test method is executed, we could decompile the generated
.class files that look like the following:
The implementation may look a bit verbose. We can tell that not only the
Sub class but also its superclasses
Root and the
java.lang.Object are rewritten. The rewritten method here depends on
JvmMockKDispatcher. If MockK is able to handle the call given the
JvmMockKDispatcher (most likely the instance is a mock), the handled result will be returned; otherwise, the original implementation would be executed and returned. The internal processing is handled inside
dispatcher.handler() , which varies depending on the context. Let’s discuss the details later.
In addition, ByteBuddy also generates subclasses when the mock target is an interface or abstract class (SubclassInstrumentation.kt). Let’s add the method
AbstractClassand decompile the dumped
.class file with
.class file becomes:
We omit its superclasses
java.lang.Objecthere since they were similar to what was described above. For the subclass
AbstractClass$Subclass0, it seems to invoke a different method
interceptNoSuper(). As a matter of fact,
interceptNoSuper() internally invokes the same method
MockK supports Android by using
java.lang.reflect.Proxy instead (and uses DexMaker to generate the DEX bytecode). As
Proxyis also used by Retrofit and other libraries, it should be familiar to most Android developers.
Objenesis: creating a mock instance
As we have discussed how ByteBuddy is used to rewrite class and generate subclass, it should be easy to understand the behavior of
verify. Before that, there is still one topic that I would like to mention: How
mockk creates new instances. Although the following example might be a bit extreme, let’s take a look:
NotImplementedError is thrown when we construct an instance of
UnimplementedClass , and we should expect the test to fail. But why does it succeed when we create the mock instance with MockK?
In addition, even if there is a non-null property, it will become null for a mock instance:
As we can see from the results above, the instance is created in a different way from the common constructor call. In fact, MockK uses Objenesis library to achieve instance creation.
Objenesis was developed by the EasyMock team. Apart from EasyMock itself, it is also used in well-known mocking libraries like Mockito. Objenesis features its support for a wide range of supported JVMs.
How does Objenesis create an instance? The approach varies with the type of JVM. For Hotspot JVM,
sun.reflect.ReflectionFactory#newConstructorForSerialization was called. As its name implies, this method creates a constructor for serialization. Other than creating an instance, it does not do anything else. As shown in the examples above, the property remains null and the init block is not executed.
Setting answers using `every`
In the previous chapter, we discussed how
mockkmethod not only creates a mock instance but also rewrites the class. How does
every method set up the answers for the specified method?
CallRecorder: Monitoring the mock instance
every method are handled internally by the class
CallRecorder manages the “current state” and the number of method calls for the mock instance.
CallRecorder is provided as thread-local variables.
In total, there are 6 types of states for
- Answering: The common state at the test runtime.
- Stubbing: The state inside
- SutbbingAwaitingAnswer: After
Stubbingbut before setting the answer by
returnsor other methods.
- Verifying: The state inside
- Exclusion: The state inside
- SafeLogging: When specific methods are invoked, like
Object#toString, there is a SafeLogging state representing before the answer is returned. Since this state is only used internally in MockK, let’s put it aside for now.
The behavior of calling a mock instance will change based on the state. When
every method is invoked,
CallRecorder changes its state from Answering to Stubbing before the block. Inside the
every block, if there is a method call on the mock instance, the information of method invocation (including the method signature, argument, and return types) are passed to the
JvmMockKDispatcher. The invocation information will be used for pattern matching later during answering.
After executing the block, the CallRecorder will change its state from Stubbing to StubbingAwaitingAnswer, waiting for the answer to be set up. After
returns or other methods to set the answer, the state changes from StubbtingAwaitingAnswer to Answering accordingly.
Sometimes multiple methods could be invoked inside the same block, but the answer is only set on the last method call. All the other methods simply return the mock instance instead. Let’s look at the following example:
The answer to
foo.bar() becomes a mock instance of
Bar$Subclass1. If we name the mock instance as
mockInstance1, the answer to
mockInstance1.value becomes the “mock” return. As shown in the example, the methods set up by
every do not need to be called consecutively.
MockKStub: Recording answers and execution history
As shown in the decompiled result earlier, the methods of the mock instance are rewritten. However, the method invocations in
verifyblock are not recorded. This is because
mockk method not only creates the mock instance, but also generates a one-to-one corresponding
MockKStubinstance, where the method invocations of
verifyblock will be recorded.
MockKStubdoes not keep a reference to the mock instance though. The relationship is managed by a
WeakMap singleton where the key is the mock instance and the value is the
WeakMap is also used to determine whether an instance is a mock instance or a regular instance.
Answering the method call
It is easy to figure out how to answer the method call, given the explanation so far.
When the method is executed in a test class, the state of
CallRecordershould be Answering. When the method of a mock instance is called in the Answering state, invocation information, such as the method arguments, is passed to
JvmMockKDispatcher, a pattern match will be performed against the answers set through
every. If the pattern matches, return the specified answer. If not, throw an
MockKException(unless for a relaxed mock instance, which creates and returns a mock).
Additionally, when a mock method is called, the call history is recorded in
Verifying the method call
verify works might not be necessary. A simple summary is: when a
verify block is executed, the state of
CallRecorder becomes Verifying. In this state, the method invocation information is recorded and verified in
Because generic type information is not available at Java runtime, the behavior of the following three tests is quite interesting:
assertEquals fails because type information is missing. Its subclass
Sealed$Subclass0 is generated and
container.sealed becomes an instance of the generated subclass type. It is the same story for
container.sealed is an instance of
ClassCastException is thrown.
On the other hand,
test3() succeeds. This is because inside the
every block, a
ClassCastException is caught, which complements the type information. The type of
return mockk() therefore would be determined at compile-time, resulting in the successful test result.
It is a good practice to not include code side-effects inside
every block. We could explore a bit more here by tweaking the test:
i become 2? This is because
every block is executed twice to complement the type information!
Although we know the subclasses of a sealed class cannot be declared outside the file, please be aware that this limitation is actually breached for the test, since a sealed class is converted to an abstract class in Java. Hence dynamic class generation technique can be used to creat subclasses, which is not limited to all the subclasses declared in the file.
spyk, mockkObject, and mockkStatic
mockkObject , and
mockkStatic are not discussed in detail in this story. But you should be able to infer their implementation by reading their documentation.
spyk is different from
mockk where the generated mock instance would proxy to the instance method. The
WeakMap mentioned in MockKStub: Recording answers and execution history would store a
SpyKStub , subclassing
MockKStub. In the case where the stubbed answer does not exist, instead of throwing an exception, the original implementation will be executed.
mockkStatic are different from
mockkin their mocking target class. In terms of the
WeakMap , the stored key becomes the
Class<*> respectively, while the stored value also becomes
SpkyKStub? As you may think, the behavior of
mockStatic is very close to the behavior of
Thank you for reading this long story. Frankly speaking, rewriting the class feels like black magic to all of us. However, the idea could be useful for implementing static mocking.
Knowing what is under the hood should be able to help you debug when things go wrong. If you are interested in this article, you might also be interested in reading the source code directly on Github.