Learning how to create a simple version of Retrofit from scratch

DALL·E — A robot wearing a wig, a mustache, and a lab coat; realistic

Mocks or Fakes? Which one do you choose? I’m kidding, I am not going to enter such a long tedious, and philosophical programming topic. The focus of this article will be on how a mock library works. After all, it looks pretty magical that libraries like Mockito and Mockk can alter the behavior of existing classes.

This article is part of a series. To see the other articles click here.

In the last article, I introduced you to the Proxy class and I showed you how Retrofit uses it to dynamically implement a REST API interface. We saw that we could use it to intercept calls to the methods of an interface. Unfortunately Proxy does not work with classes, which is essential to a proper mock library. In this article, we are going to see how we can overcome that limitation with bytecode generation libraries.

As mentioned by Fabian Lange, “An often overlooked feature of the Java platform is the ability to modify a program’s bytecode before it is executed by the JVM’s interpreter or just-in-time (JIT) compiler”. Such a feature is explored by bytecode generation libraries which allow the development of code capable of creating new classes or modifying existing ones at runtime. That, in turn, enables a wide range of applications such as mocks, profiling, security, and aspect-oriented programming, among others.

One of the most popular code generation libraries were cglib, which stands for code generation library. Let’s see how we could implement an approach similar to the Proxy class using cglib:

Simple mock implementation with cglib

On line 10, I created the inline reified function createMock, so I can access the Class object. The Enhancer class generates dynamic subclasses to enable method interception. In line 11, I set the superclass to be the type to be mocked — in fact, we are creating a subclass. In line 12, I set the callback to interceptor, which will work similarly to the InvocationHandler that we used with Proxy in the previous article. Whenever a method on the mocked object is called, interceptor will called with that method and its arguments. Line 13 creates an instance of the dynamically defined subclass and casts it to the mocked typed. Finally, lines 16 through 23 implement the MethodInterceptor, which uses reflection to check the method name and return the mocked response.

I hope the cglib example gave you a basic idea of how we will implement our simple version of Mockito/Mockk. If you paid attention, I said cglib was one of the most popular code generation libraries. It was because it is no longer being maintained and may not work with newer JDKs. The recommendation now is to use ByteBuddy. In fact, that is the library that powers Mockito and Mockk, the mocking frameworks you certainly used if you wrote unit tests for Java, Kotlin, or Android. With ByteBuddy we can create subclasses, override methods, define new classes, delegate, or even redefine existing classes.

As an example of what ByteBudy can do, we will re-implement the following class Person at runtime:

class Person(a: String, b: String) {
private val firstName: String = a
private val lastName: String = b
override fun toString(): String {
return "$firstName $lastName is the person's name"
}
}

First, we add the dependencies to our build.gradle.kts file (versions were the latest at the time of writing this article).

dependencies {
...
implementation("net.bytebuddy:byte-buddy:1.14.14")
implementation("net.bytebuddy:byte-buddy-agent:1.14.14")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")
...
}

And here is the code:

Class Person dynamically created with ByteBuddy

Most of the code should be straightforward:

  • Lines 13 and 14 define the new class. It is named Person and it is a subclass of Any like any other Kotlin class.
  • Lines 15 and 16 define the fields firstName and lastName.
  • Lines 17–23 define the constructor. The most important line here is the one that calls intercept . That is the function that specifies the implementation of the method. In this case, we first call the constructor of Any, the superclass, and then we set the firstName and lastName fields with the parameters that were passed to the constructor being defined.
  • Lines 24 and 25 override Any::toString() by delegating it to the PersonToString object. The FieldValue annotations tells ByteBudy to bind the parameters of the function with fields of the class being defined.
  • Lines 26 to 28 finish the creation of the class and load it into the classpath, so it is available to be used.
  • Lines 39 to 46 show how to use the newly defined class. We need to use reflection to create an instance and call the methods. The output of those lines is the following:
Person
[private java.lang.String Person.firstName, private java.lang.String Person.lastName]
John Smith is the person's name

Now that we have a basic idea of how to use ByteBuddy, let’s try to create our own mock library. We will implement a very simple version of Mockk and Mockito that could be used like this:

val mocked: ClassToBeMocked = mock {
whenever(ClassToBeMocked::method) returns returnValue
}

Implementing the DSL

The first step is to define the DSL to collect the mock configurations.

interface MockScopeDsl {
fun <T> whenever(method: KFunction<T>): KFunction<T>
infix fun <T> KFunction<T>.returns(returnValue: T)
}

class MockScopeDslImpl : MockScopeDsl {
val mockedMethods = mutableMapOf<KFunction<*>, Any?>()
override fun <T> whenever(method: KFunction<T>) = method
override fun <T> KFunction<T>.returns(returnValue: T) {
mockedMethods[this@returns] = returnValue
}
}

The mock scope of our DSL has two functions. The functionwhenever does not do much. It just returns its argument. However, it makes the whole expression more legible and it forces the expression to start with a KFunction, the Kotlin equivalent to Java’s Method. The infix extension returns stores the function and the return value in a map.

Implementing the mock function

Now that we have a nice DSL, let’s implement the function that creates the mock.

inline fun <reified T : Any> mock(configBlock: MockScopeDsl.() -> Unit): T {
val mockScope = MockScopeDslImpl()
mockScope.configBlock()
var subClassProto: DynamicType.Builder<T> = ByteBuddy().subclass(T::class.java)
mockScope.mockedMethods.forEach { (kFunction, returnValue) ->
subClassProto = subClassProto
.method(ElementMatchers.`is`(kFunction.javaMethod!!))
.intercept(FixedValue.value(returnValue!!))
}
val clazz = subClassProto.make().load(T::class.java.classLoader).loaded as Class<T>

return clazz.getDeclaredConstructor().newInstance()
}

The mock function is inlined reified so we can have access to the Class object of the class we want to mock. The function receives the DSL configBlock and calls it on an instance of MockScopeDslImpl to collect the mocked methods on a map. We then dynamically create a subclass of the class to be mocked using ByteBuddy. Next, we iterate over the map of mocked methods telling ByteBuddy to intercept the respective method and return the value defined by the DSL. And finally, we finish the building of the mock class and load it into the classpath.

The full implementation

Here is the complete code for our mock library including a sample JUnit test to demonstrate its usage

Source link