Unraveling MockK’s black magic

Chao Zhang
8 min readJan 2, 2021

--

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 every and 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:

class OpenClass
class FinalClass
class AbstractClass$Subclass0
class Interface$Subclass1

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, AbstractClass$Subclass0 and 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:

// build.gradle
test {
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.

Once the class transform test method is executed, we could decompile the generated .class files that look like the following:

1/Sub.class
2/Root.class
3/java/lang/Object.class

The implementation may look a bit verbose. We can tell that not only the Sub class but also its superclassesRoot and the java.lang.Object are rewritten. The rewritten method here depends onJvmMockKDispatcher. 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 foo()to AbstractClassand decompile the dumped .class file with io.mockk.classdump.path enabled.

The decompiled .class file becomes:

We omit its superclasses AbstractClass and 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 dispatcher.handler().

MockK supports Android by usingjava.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 every and 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

JvmMockKDispatcher and every method are handled internally by the class CallRecorder. CallRecorder manages the “current state” and the number of method calls for the mock instance.

Note: CallRecorder is provided as thread-local variables.

In total, there are 6 types of states for CallRecorder:

  • Answering: The common state at the test runtime.
  • Stubbing: The state inside every block.
  • SutbbingAwaitingAnswer: After Stubbing but before setting the answer by returns or other methods.
  • Verifying: The state inside verify block.
  • Exclusion: The state inside excludeRecords block.
  • 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.
State transition of CallRecorder

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 CallRecorder through 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 everyblock and verifyblock are not recorded. This is because mockk method not only creates the mock instance, but also generates a one-to-one correspondingMockKStubinstance, where the method invocations of everyblock and 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 MockKStub. This 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 CallRecorderthrough JvmMockKDispatcher, a pattern match will be performed against the answers set throughevery. 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 MockKStub.

Verifying the method call

Explaining how 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 MockKStub.

Generics

Because generic type information is not available at Java runtime, the behavior of the following three tests is quite interesting:

In test1(), 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 test2(), container.sealed is an instance of Sealed$Subclass0, therefore 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:

Why would 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

spyk , 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.

mockkObject and mockkStatic are different from mockkin their mocking target class. In terms of the WeakMap , the stored key becomes the object or Class<*> respectively, while the stored value also becomes SpkyKStub.

Why SpkyKStub? As you may think, the behavior of mockkObject and mockStatic is very close to the behavior of spyk

Summary

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.

--

--

Chao Zhang
Chao Zhang

Responses (1)