📜 ⬆️ ⬇️

MockK - library for mocking in Kotlin

MockK logo Kotlin is still a very new technology and this means that there are many opportunities to make something better. For me, this path was so. I started writing a simple web processing layer on Netty and coroutine ah. Everything was fine, I even did something like a web framework with routing, web sockets, DSL and full asynchrony. For the first time, everything seemed easy to learn. Indeed, coroutines make callbacks of callbacks linear and readable code.


A surprise was waiting for me when I started testing it all. It turns out that Kotlin and mocking are hard compatible things. Primarily due to final fields. Further, there is exactly one library for testing the cauldron and this is the Mockito. For her, created a wrapper that provides something like DSL. But here, not everything went smoothly. First of all, it is testing functions with named parameters. Mockito requires you to set absolutely all parameters in the form of matchers, and in Kotlin there are often many of these parameters and some of them have default values. To set them all is too expensive. In addition, often the last parameter is the lambda block. Create an ArgumentCaptor and perform complex casting in order to call it - bust. The cortices themselves are functions with the last parameter of the Continuation type. And it requires special handling. In Mockito, they added it, but did not add the convenience of calling the most Corutin. So, from all these little things, it seems that this wrapper does not quite fit harmoniously into the language.


Having estimated the amount of work, I came to the conclusion that one person could easily cope and began to write his own library. I tried to make it close to the language and solve the problems that I encountered when testing.


Now I will tell you what happened to me. Here is the simplest example for starters:


val car = mockk<Car>() every { car.drive(Direction.NORTH) } returns Outcome.OK car.drive(Direction.NORTH) // returns OK verify { car.drive(Direction.NORTH) } 

Matchers are not used here, just the DSL syntax is shown as a whole. First, the every / returns block specifies what the mock should return, and the verify block to check if the call was made.


Of course, MockK has the ability to capture variables, a lot of matcher, spy, and other constructions. Here is a more detailed example . All this is also in the Mockito. Therefore, I would like to describe the differences.


Tool So in order for all this to work, I needed a Java Agent to remove all final attributes. This is not at all difficult, works from Maven / Gradle, but does not work very well with IDE. Each time you need to assign the “-javaagent: <some path>” parameter. There was even an idea to write plugins for popular IDEs that make it easy to run Java agents. But as a result, I had to make support for launching JUnit4 and JUnit5 without the Java Agent.


For JUnit4, this is a launch using the standard @RunWith annotation, which I don’t like myself, but there’s nowhere to go. In order to somehow make life easier, I added ChainedRunWith. It allows you to set the next Runner in the chain and thus use two different libraries.


For JUnit5, it is enough to add a dependency on a JAR with an agent, and all the magic will happen by itself. But I can say that the implementation is a real hack with Unsafe, Javassist and Reflection. For this reason, the official launch method is still considered launch via the Java Agent.


The next feature is the ability to set not all parameters, like matchers, but only some of them. To realize this opportunity, I had to think about it. If we have this function:


 fun response(html: String = "", contentType: String = "text/html", charset: Charset = Charset.forName("UTF-8"), status: HttpResponseStatus = HttpResponseStatus.OK) 

And somewhere there is his challenge:


 response(“Great”) 

In order to test this in the Mockito, you must specify all the parameters:


 `when`(mock.response(eq(“Great”), eq("text/html"), eq(Charset.forName(“UTF-8”)), eq(HttpResponseStatus.OK)))).doNothing() 

This is clearly limiting. In MockK, you can specify only necessary matcher s, all other parameters will be replaced by eq (...) or, if matcher allAny () is specified, then any ().


 every { response(“Great”) } answers { nothing } every { response(eq(“Great”)) } answers { nothing } every { response(eq(“Great”), allAny()) } answers { nothing } 

Idea This is achieved in such a trick: the every block is called several times and each time the matcher returns a random value, then the data is matched and the necessary matcher is found. For those places where a matcher is not specified, the argument will almost always be constant. “Almost always” because sometimes the default parameter will be a function that returns time or something similar. It's easy to get around the explicit indication of the matcher.


Read more about DSL testing. For example, consider the following code:


 fun jsonResponse(block: JsonScope.() -> Unit) { val str = StringBuilder() JsonScope(str).block() response(str.toString(), "application/json") } jsonResponse { seq { proxyOps.allConnections().forEach { hash { "listenPort"..it.listenPort "connectHost"..it.connectHost "connectPort"..it.connectPort } } } } 

It does not matter what he is doing now - it is important that this is a composition of constructions from DSL, collecting JSON.


How to test it? In MockK, there is a special matcher captureLambda for this. Convenience is that we can capture lambd in one expression and call it in the answer:


 val strBuilder = StringBuilder() val jsonScope = JsonScope(strBuilder) every { scope.jsonResponse(captureLambda(Function1::class)) } answers { lambda(jsonScope) } 

To verify that the underlying code is correct, you can compare the contents of the StringBuilder with a sample of what should be in the response. The convenience is only that the block passed as the last parameter is an idiom of the language, and it is convenient to have a special way of processing it in a mocking framework.


Coroutine support is also not so difficult to implement a function as just a convenient way to do what the language represents out of the box. Simply replace the call to every and verify with coEvery and coVerify and can call coroutine inside.


 suspend fun jsonResponse(block: JsonScope.() -> Unit) { val str = StringBuilder() JsonScope(str).block() response(str.toString(), "application/json") } coVerify { scope.jsonResponse(any()) } 

As a result, the goal of the project is to make mocking in Kotlin as convenient as possible, and not to increase the thousands of functions that PowerMock and Mockito have. To this I will strive further.


Exit

I ask the public not to judge strictly, try the library in their projects and suggest new functions, bring to mind the current ones.


Project website: http://mockk.io


')

Source: https://habr.com/ru/post/341202/


All Articles