Testing in Kotlin with Spock

The purpose of the article is to show what difficulties arise when using Spock with Kotlin, what are the ways to resolve them and answer the question whether it is worth using Spock if you are developing on Kotlin. Details under the cut.

I work for a company that practices extreme programming . One of the main methods of extreme programming that we use in our daily work is TDD (test-driven development). This means that before changing the code, we write a test that covers the desired change. Thus, we regularly write tests and have code coverage with tests close to 100%. This makes certain requirements for choosing a test framework: it is one thing to write tests once a week, it is quite another to do it every day.

We are developing on Kotlin and at some point we chose Spock as the main framework. About 6 months have passed since that moment, a sense of euphoria and novelty has passed, so the article is a kind of retrospective in which I will try to tell you what difficulties we encountered during this time and how we resolved them.

Why exactly Spock?




First of all, you need to figure out which frameworks allow Kotlin to be tested and what advantages Spock gives in comparison with them.

One of the advantages of Kotlin is its compatibility with Java, which allows you to use any Java frameworks for testing, such as Junit , TestNG , Spock , etc. At the same time, there are frameworks designed specifically for Kotlin such as Spek and Kotest . Why did we choose Spock?

I would highlight the following advantages:

  • First, Spock is written in Groovy. Good or bad - judge for yourself. About Groovy, you can read the article here . Groovy personally attracts me to the laconic syntax, the presence of dynamic typing, the built-in syntax for lists, regular and associative arrays, and completely crazy type conversions.
  • Secondly, Spock already contains a mock framework (MockingApi) and an assertion library;
  • Spock is also great for writing parameterized tests:

def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c

    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }

This is a very superficial comparison (if it can be called a comparison at all), but these are roughly the reasons why we chose Spock. Naturally, in the following weeks and months we encountered some difficulties.

Problems encountered when testing Kotlin with Spock and solving them


Here I immediately make a reservation that the problems described below are not unique to Spock, they will be relevant for any Java framework and they are mainly associated with compatibility with Kotlin .

1. final by default


Problem

Unlike Java, all Kotlin classes have a modifier by default final, which prevents further descendants and method overrides. The problem here is that when creating mock objects of any class, most mock frameworks try to create a proxy object, which is just a descendant of the original class and overrides its methods.

Therefore, if you have a service:

class CustomerService {
    fun getCustomer(id: String): Customer {
        // Some logic
    }
}

and you try to create a mock of this service in Groovy:

def customerServiceMock = Mock(CustomerService)

then you get an error:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes

Decision:

  • , , open, . ยซยป , , , , ;
  • all-open . . Spring Framework Hibernate, cglib(Code Generation Library);
  • , mock-, Kotlin (, mockk). , , Spock Spock MockingApi.

2.


Problem

In Kotlin, function or constructor arguments can have default values โ€‹โ€‹that are used if the function argument is not specified when it is called. The difficulty is that Java bytecode loses default argument values โ€‹โ€‹and function parameter names, and therefore, when calling a function or Kotlin constructor from Spock, all values โ€‹โ€‹must be explicitly specified.

Consider a class Customerthat has a constructor with 2 mandatory fields emailand nameand optional fields that have default values:

data class Customer(
    val email: String,
    val name: String,
    val surname: String = "",
    val age: Int = 18,
    val identifiers: List<NationalIdentifier> = emptyList(),
    val addresses: List<Address> = emptyList(),
    val paymentInfo: PaymentInfo? = null
)

In order to create an instance of this class in the Groovy Spock test, you will have to pass values โ€‹โ€‹for all arguments to the constructor:

new Customer("john.doe@gmail.com", "John", "", 18, [], [], null)

Decision:


class ModelFactory {
    static def getCustomer() { 
        new Customer(
            "john.doe@gmail.com", 
            "John", 
            "Doe", 
            18, 
            [nationalIdentifier], 
            [address], 
            paymentInfo
        ) 
    }

    static def getAddress() { new Address(/* arguments */) }

    static def getNationalIdentifier() { new NationalIdentifier(/* arguments */) }

    static def getPaymentInfo() { new PaymentInfo(/* arguments */) }
}

3. Java Reflection vs Kotlin Reflection


This may be a fairly narrow area, but if you plan to use reflection in your tests, you should especially think about whether to use Groovy. In our project, we use a lot of annotations and, in order not to forget to annotate certain classes or certain fields with annotations, we write tests using reflection. This is where difficulties arise.

Problem

Classes and annotations are written in Kotlin, logic related to tests and reflection in Groovy. Because of this, the tests produce a hash from the Java Reflection API and Kotlin Reflection:

Converting Java classes to Kotlin:

def kotlinClass = new KClassImpl(clazz)

Calling Kotlin extension functions as static methods:
ReflectJvmMapping.getJavaType(property.returnType)

Hard-to-read code where Kotlin types will be used:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
    // Some logic
}

Of course, this is not something excessive or impossible. The only question is, isnโ€™t it easier to do this with the Kotlin testing framework, where it will be possible to use extension functions and pure Kotlin Reflection ??

4. Coroutines


If you plan to use coroutines, then this is another reason to think about whether you want to choose the Kotlin framework for testing.

Problem

If you create a suspendfunction:

suspend fun someSuspendFun() {
    // Some logic
}

, then it compiles as follows:

public void someSuspendFun(Continuation<? super Unit> $completion) {
    // Some logic
}

where Continuation is a Java class that encapsulates the logic for executing coroutine. This is where the problem arises, how to create an object of this class? Also, runBlocking is not available in Groovy . And so itโ€™s generally not very clear how to test the code with coroutines in Spock.

Summary


Over six months of using Spock, it brought us more good than bad and we do not regret the choice: tests are easy to read and maintain, writing parameterized tests is a pleasure. A fly in the ointment in our case turned out to be reflection, but it is used in only a few places, the same with coroutines.

When choosing a framework for testing a project that is being developed on Kotlin, you should think carefully about what language features you plan to use. In case you are writing a simple CRUD application, Spock is the perfect solution. Do you use coroutines and reflection? Then better look at Spek or Kotest .

The material does not claim to be complete, so if you have encountered other difficulties when using Spock and Kotlin - write in the comments.

useful links



All Articles