Testen in Kotlin mit Spock

Der Artikel soll zeigen, welche Schwierigkeiten bei der Verwendung von Spock mit Kotlin auftreten, wie diese behoben werden können, und die Frage beantworten, ob es sich lohnt, Spock zu verwenden, wenn Sie auf Kotlin entwickeln. Details unter dem Schnitt.

Ich arbeite für ein Unternehmen, das extreme Programmierung praktiziert . Eine der Hauptmethoden für extreme Programmierung, die wir in unserer täglichen Arbeit verwenden, ist TDD (testgetriebene Entwicklung). Dies bedeutet, dass wir vor dem Ändern des Codes einen Test schreiben, der die gewünschte Änderung abdeckt. Daher schreiben wir regelmäßig Tests und haben eine Codeabdeckung mit Tests nahe 100%. Dies stellt bestimmte Anforderungen an die Auswahl eines Testframeworks: Es ist eine Sache, Tests einmal pro Woche zu schreiben, es ist eine ganz andere, dies jeden Tag zu tun.

Wir entwickeln auf Kotlin und haben irgendwann Spock als Hauptrahmen gewählt. Seit diesem Moment sind ungefähr 6 Monate vergangen, ein Gefühl von Euphorie und Neuheit ist vergangen, daher ist der Artikel eine Art Rückblick, in dem ich versuchen werde, Ihnen zu sagen, auf welche Schwierigkeiten wir in dieser Zeit gestoßen sind und wie wir sie gelöst haben.

Warum genau Spock?




Zunächst müssen Sie herausfinden, mit welchen Frameworks Kotlin getestet werden kann und welche Vorteile Spock im Vergleich zu diesen bietet.

Einer der Vorteile von Kotlin ist die Kompatibilität mit Java, mit der Sie beliebige Java-Frameworks zum Testen verwenden können, z. B. Junit , TestNG , Spock usw. Gleichzeitig gibt es Frameworks, die speziell für Kotlin entwickelt wurden, wie Spek und Kotest . Warum haben wir uns für Spock entschieden?

Ich möchte die folgenden Vorteile hervorheben:

  • Zunächst wird Spock in Groovy geschrieben. Gut oder schlecht - urteilen Sie selbst. Über Groovy können Sie den Artikel hier lesen . Groovy persönlich zieht mich zur lakonischen Syntax, zum Vorhandensein dynamischer Typisierung, zur integrierten Syntax für Listen, zu regulären und assoziativen Arrays und zu völlig verrückten Typkonvertierungen.
  • Zweitens enthält Spock bereits ein Mock-Framework (MockingApi) und eine Assertion-Bibliothek.
  • Spock eignet sich auch hervorragend zum Schreiben parametrisierter 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
  }

Dies ist ein sehr oberflächlicher Vergleich (wenn man ihn überhaupt als Vergleich bezeichnen kann), aber dies sind ungefähr die Gründe, warum wir uns für Spock entschieden haben. Natürlich stießen wir in den folgenden Wochen und Monaten auf einige Schwierigkeiten.

Probleme beim Testen und Lösen von Kotlin mit Spock


Hier mache ich sofort einen Vorbehalt, dass die unten beschriebenen Probleme nicht nur für Spock gelten, sondern für jedes Java-Framework relevant sind und hauptsächlich mit der Kompatibilität mit Kotlin zusammenhängen .

1. standardmäßig final


Problem

Im Gegensatz zu Java verfügen alle Kotlin-Klassen standardmäßig über einen Modifikator final, der weitere Nachkommen und Methodenüberschreibungen verhindert. Das Problem hierbei ist, dass die meisten Schein-Frameworks beim Erstellen von Scheinobjekten einer Klasse versuchen, ein Proxy-Objekt zu erstellen, das nur ein Nachkomme der ursprünglichen Klasse ist und deren Methoden überschreibt.

Wenn Sie also einen Service haben:

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

und Sie versuchen, einen Mock dieses Dienstes in Groovy zu erstellen:

def customerServiceMock = Mock(CustomerService)

dann erhalten Sie eine Fehlermeldung:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes

Entscheidung:

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

2.


Problem

In Kotlin können Funktions- oder Konstruktorargumente Standardwerte haben, die verwendet werden, wenn das Funktionsargument beim Aufruf nicht angegeben wird. Die Schwierigkeit besteht darin, dass Java-Bytecode Standardargumentwerte und Funktionsparameternamen verliert. Daher müssen beim Aufrufen einer Kotlin-Funktion oder eines Kotlin-Konstruktors von Spock alle Werte explizit angegeben werden.

Betrachten wir eine Klasse , Customerdie einen Konstruktor mit 2 Pflichtfelder hat emailund nameund optionale Felder , die Standardwerte haben:

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
)

Um eine Instanz dieser Klasse im Groovy Spock-Test zu erstellen, müssen Sie Werte für alle Argumente an den Konstruktor übergeben:

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

Entscheidung:


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


Dies mag ein ziemlich enger Bereich sein, aber wenn Sie in Ihren Tests Reflexion verwenden möchten, sollten Sie besonders darüber nachdenken, ob Sie Groovy verwenden sollen. In unserem Projekt verwenden wir viele Anmerkungen. Um nicht zu vergessen, bestimmte Klassen oder bestimmte Felder mit Anmerkungen zu versehen, schreiben wir Tests mit Reflexion. Hier entstehen Schwierigkeiten.

Problemklassen

und Anmerkungen sind in Kotlin geschrieben, Logik in Bezug auf Tests und Reflexion in Groovy. Aus diesem Grund erzeugen die Tests einen Hash aus der Java Reflection-API und Kotlin Reflection:

Konvertieren von Java-Klassen in Kotlin:

def kotlinClass = new KClassImpl(clazz)

Das Aufrufen der Kotlin-Erweiterung funktioniert als statische Methode:
ReflectJvmMapping.getJavaType(property.returnType)

Schwer lesbarer Code, in dem Kotlin-Typen verwendet werden:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
    // Some logic
}

Dies ist natürlich nicht übertrieben oder unmöglich. Die Frage ist nur, ob es nicht einfacher ist, dies mit dem Kotlin-Testframework zu tun, bei dem Erweiterungsfunktionen und reine Kotlin-Reflexion verwendet werden können.

4. Coroutinen


Wenn Sie Coroutinen verwenden möchten, ist dies ein weiterer Grund, darüber nachzudenken, ob Sie das Kotlin-Framework zum Testen auswählen möchten.

Problem

Wenn Sie eine suspendFunktion erstellen :

suspend fun someSuspendFun() {
    // Some logic
}

, dann kompiliert es wie folgt:

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

Dabei ist Continuation eine Java-Klasse, die die Logik zum Ausführen von Coroutine kapselt. Hier tritt das Problem auf, wie man ein Objekt dieser Klasse erstellt. Außerdem ist runBlocking in Groovy nicht verfügbar . Daher ist es im Allgemeinen nicht ganz klar, wie der Code mit Coroutinen in Spock getestet werden soll.

Zusammenfassung


Über sechs Monate mit Spock hat es uns mehr Gutes als Schlechtes gebracht und wir bereuen die Wahl nicht: Tests sind einfach zu lesen und zu warten, das Schreiben parametrisierter Tests ist ein Vergnügen. Eine Fliege in der Salbe stellte sich in unserem Fall als Reflexion heraus, wird aber nur an wenigen Stellen verwendet, genau wie bei Coroutinen.

Wenn Sie ein Framework zum Testen eines Projekts auswählen, das auf Kotlin entwickelt wird, sollten Sie sorgfältig überlegen, welche Sprachfunktionen Sie verwenden möchten. Wenn Sie eine einfache CRUD-Anwendung schreiben, ist Spock die perfekte Lösung. Verwenden Sie Coroutinen und Reflexion? Dann schauen Sie sich besser Spek oder Kotest an .

Das Material erhebt keinen Anspruch auf Vollständigkeit. Wenn Sie also bei der Verwendung von Spock und Kotlin auf andere Schwierigkeiten gestoßen sind, schreiben Sie in die Kommentare.

Nützliche Links



All Articles