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 method actually do? How do and 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 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 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, and , 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 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 test method is executed, we could decompile the generated 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 class but also its superclasses and the are rewritten. The rewritten method here depends on. If MockK is able to handle the call given the (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 , 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 to and decompile the dumped file with enabled.

The decompiled file becomes:

We omit its superclasses and here since they were similar to what was described above. For the subclass , it seems to invoke a different method . As a matter of fact, internally invokes the same method .

MockK supports Android by using instead (and uses DexMaker to generate the DEX bytecode). As is 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 and . Before that, there is still one topic that I would like to mention: How creates new instances. Although the following example might be a bit extreme, let’s take a look:

is thrown when we construct an instance of , 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, 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 method not only creates a mock instance but also rewrites the class. How does method set up the answers for the specified method?

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

Note: 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 block.
  • SutbbingAwaitingAnswer: After but before setting the answer by or other methods.
  • Verifying: The state inside block.
  • Exclusion: The state inside block.
  • SafeLogging: When specific methods are invoked, like , 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 method is invoked, changes its state from Answering to Stubbing before the block. Inside the 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 through . 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 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 becomes a mock instance of . If we name the mock instance as , the answer to becomes the “mock” return. As shown in the example, the methods set up by 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 block and block are not recorded. This is because method not only creates the mock instance, but also generates a one-to-one correspondinginstance, where the method invocations of block and block will be recorded.

does not keep a reference to the mock instance though. The relationship is managed by a singleton where the key is the mock instance and the value is the . This 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 should 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 through , a pattern match will be performed against the answers set through. If the pattern matches, return the specified answer. If not, throw an (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

Explaining how works might not be necessary. A simple summary is: when a block is executed, the state of becomes Verifying. In this state, the method invocation information is recorded and verified in .

Generics

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

In , fails because type information is missing. Its subclass is generated and becomes an instance of the generated subclass type. It is the same story for , is an instance of , therefore is thrown.

On the other hand, succeeds. This is because inside the block, a is caught, which complements the type information. The type of 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 block. We could explore a bit more here by tweaking the test:

Why would become 2? This is because 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

, , and are not discussed in detail in this story. But you should be able to infer their implementation by reading their documentation.

is different from where the generated mock instance would proxy to the instance method. The mentioned in MockKStub: Recording answers and execution history would store a , subclassing . In the case where the stubbed answer does not exist, instead of throwing an exception, the original implementation will be executed.

and are different from in their mocking target class. In terms of the , the stored key becomes the or respectively, while the stored value also becomes .

Why As you may think, the behavior of and is very close to the behavior of

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.

Android Developer@LinkedIn

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store