Let Kotlin do the code for you — Part III: Mockito, Mockk, and Code Generation | by André Oriani | May, 2024
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
:
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:
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
andlastName
. - 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 ofAny
, the superclass, and then we set thefirstName
andlastName
fields with the parameters that were passed to the constructor being defined. - Lines 24 and 25 override
Any::toString()
by delegating it to thePersonToString
object. TheFieldValue
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
Related Posts
Leave a Reply Cancel reply
Categories
- ! Без рубрики (1)
- ++PU (1)
- 1 (1)
- 1w (1)
- 1win Brazil (1)
- 1win India (1)
- 1WIN Official In Russia (1)
- 1win Turkiye (1)
- 1xbet egypt (1)
- 2ankarafayansustasi.net_may (1)
- ankarafayansustasi.netsiteai apr (1)
- Artificial intelligence (1)
- Arts & Entertainment, Photography (1)
- belugasitesi_mAY (1)
- BH_TOPsitesi apr (1)
- BHsitesy_may (2)
- Blog (3)
- Bookkeeping (14)
- Bootcamp de programação (2)
- Bootcamp de programación (2)
- BT_TOPsitesi apr (1)
- casino (5)
- casinom-hub (1)
- casinom-hub.comsitesi apr (3)
- colombian mail order brides (1)
- Cryptocurrency exchange (2)
- Dinamobet_next (1)
- Disease & Illness, Colon Cancer (1)
- Dumanbet (1)
- Dumanbet_next (1)
- Finance, Insurance (1)
- FinTech (5)
- Forex Trading (11)
- Galabet (1)
- Health & Fitness, Fitness Equipment (1)
- Hitbet (1)
- Home & Family, Crafts (1)
- Home & Family, Gardening (1)
- Internet Business, Audio-Video Streaming (1)
- Internet Business, Ecommerce (1)
- Internet Business, Email Marketing (1)
- Internet Business, Internet Marketing (1)
- IT Вакансії (1)
- IT Образование (5)
- IT Освіта (1)
- latin women dating (1)
- mail order bride (1)
- Mars bahis (2)
- Matadorbet (1)
- minimiri.comsitesi apr (3)
- Mobile App Development (771)
- Mostbet Russia (1)
- New Post (1)
- News (12)
- PB_TOPsitesi apr (1)
- PBsitesi_may (1)
- Pusulabet (1)
- redmirepool.bizsitesi apr (2)
- redmirepoolsitesi_may (1)
- Reference & Education, College (1)
- Reference & Education, Sociology (1)
- Rokusitesi apr (1)
- Sober living (6)
- Society, Divorce (1)
- Software development (7)
- Superbetin (1)
- Tempobet_next (1)
- thelongeststride.comsitesi apr (1)
- tipobet-turkiyesitesi apr (1)
- Ultrabet (1)
- Uncategorized (1)
- Игра (2)
- казино (1)
- Криптовалюты (1)
- Новости Криптовалют (1)
- Финтех (7)
- Форекс Брокеры (9)
- Форекс обучение (2)