Pruebas en Kotlin con Spock

El propósito del artículo es mostrar qué dificultades surgen al usar Spock con Kotlin, cuáles son las formas de resolverlas y responder a la pregunta de si vale la pena usar Spock si está desarrollando Kotlin. Detalles debajo del corte.

Trabajo para una empresa que practica programación extrema . Uno de los principales métodos de programación extrema que utilizamos en nuestro trabajo diario es TDD (desarrollo basado en pruebas). Esto significa que antes de cambiar el código, escribimos una prueba que cubra el cambio deseado. Por lo tanto, escribimos pruebas regularmente y tenemos cobertura de código con pruebas cercanas al 100%. Esto exige ciertos requisitos para elegir un marco de prueba: una cosa es escribir pruebas una vez por semana, y otra muy distinta hacerlo todos los días.

Estamos desarrollando en Kotlin y en algún momento elegimos a Spock como marco principal. Han pasado aproximadamente 6 meses desde ese momento, ha pasado una sensación de euforia y novedad, por lo que el artículo es una especie de retrospectiva en la que intentaré decirle qué dificultades encontramos durante este tiempo y cómo las resolvimos.

¿Por qué exactamente Spock?




En primer lugar, debe averiguar qué marcos permiten probar Kotlin y qué ventajas ofrece Spock en comparación con ellos.

Una de las ventajas de Kotlin es su compatibilidad con Java, que le permite utilizar cualquier marco de Java para realizar pruebas, como Junit , TestNG , Spock , etc. Al mismo tiempo, hay marcos diseñados específicamente para Kotlin como Spek y Kotest . ¿Por qué elegimos Spock?

Destacaría las siguientes ventajas:

  • Primero, Spock está escrito en Groovy. Bueno o malo, juzga por ti mismo. Sobre Groovy, puedes leer el artículo aquí . Groovy personalmente me atrae a la sintaxis lacónica, la presencia de tipeo dinámico, la sintaxis incorporada para listas, matrices regulares y asociativas y conversiones de tipos completamente locas.
  • En segundo lugar, Spock ya contiene un marco simulado (MockingApi) y una biblioteca de aserciones;
  • Spock también es excelente para escribir pruebas parametrizadas:

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

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

Esta es una comparación muy superficial (si se puede llamar una comparación), pero estas son más o menos las razones por las que elegimos Spock. Naturalmente, en las siguientes semanas y meses encontramos algunas dificultades.

Problemas encontrados al probar Kotlin con Spock y resolverlos


Aquí inmediatamente hago una reserva de que los problemas descritos a continuación no son exclusivos de Spock, serán relevantes para cualquier marco de Java y están asociados principalmente con la compatibilidad con Kotlin .

1. final por defecto


Problema

A diferencia de Java, todas las clases de Kotlin tienen un modificador por defecto final, lo que evita futuros descendientes y anulaciones de métodos. El problema aquí es que al crear objetos simulados de cualquier clase, la mayoría de los marcos simulados intentan crear un objeto proxy, que es solo un descendiente de la clase original y anula sus métodos.

Por lo tanto, si tiene un servicio:

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

e intentas crear un simulacro de este servicio en Groovy:

def customerServiceMock = Mock(CustomerService)

entonces obtienes un error:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes

Decisión:

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

2.


Problema

En Kotlin, los argumentos de función o constructor pueden tener valores predeterminados que se usan si el argumento de función no se especifica cuando se llama. La dificultad es que el código de bytes de Java pierde los valores de argumento predeterminados y los nombres de los parámetros de la función y, por lo tanto, cuando se llama a una función o al constructor de Kotlin desde Spock, todos los valores deben especificarse explícitamente.

Considere una clase Customerque tiene un constructor con 2 campos obligatorios emaily namey campos opcionales que tienen valores por defecto:

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
)

Para crear una instancia de esta clase en la prueba de Groovy Spock, deberá pasar valores para todos los argumentos al constructor:

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

Decisión:


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


Esta puede ser un área bastante estrecha, pero si planea usar la reflexión en sus pruebas, debe pensar especialmente si debe usar Groovy. En nuestro proyecto, usamos muchas anotaciones y, para no olvidar anotar ciertas clases o ciertos campos con anotaciones, escribimos pruebas usando la reflexión. Aquí es donde surgen las dificultades. Las clases de

problemas

y las anotaciones están escritas en Kotlin, la lógica relacionada con las pruebas y la reflexión en Groovy. Debido a esto, las pruebas producen un hash de la API de Java Reflection y Kotlin Reflection:

Convertir clases de Java a Kotlin:

def kotlinClass = new KClassImpl(clazz)

Llamar a la extensión de Kotlin funciona como métodos estáticos:
ReflectJvmMapping.getJavaType(property.returnType)

Código difícil de leer donde se utilizarán los tipos de Kotlin:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
    // Some logic
}

Por supuesto, esto no es algo excesivo o imposible. La única pregunta es, ¿no es más fácil hacer esto con el marco de prueba de Kotlin, donde será posible usar funciones de extensión y Kotlin Reflection puro?

4. Corutinas


Si planea usar corutinas, esta es otra razón para pensar si desea elegir el marco de Kotlin para las pruebas.

Problema

Si crea una suspendfunción:

suspend fun someSuspendFun() {
    // Some logic
}

, luego se compila de la siguiente manera:

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

donde Continuation es una clase Java que encapsula la lógica para ejecutar la rutina. Aquí es donde surge el problema, ¿cómo crear un objeto de esta clase? Además, runBlocking no está disponible en Groovy . Por lo tanto, en general no está muy claro cómo probar el código con corutinas en Spock.

Resumen


Más de seis meses de uso de Spock, nos trajo más bien que mal y no nos arrepentimos de la elección: las pruebas son fáciles de leer y mantener, escribir pruebas parametrizadas es un placer. Una mosca en la pomada en nuestro caso resultó ser reflejo, pero se usa solo en unos pocos lugares, lo mismo con las corutinas.

Al elegir un marco para probar un proyecto que se está desarrollando en Kotlin, debe pensar cuidadosamente sobre las características de lenguaje que planea usar. En caso de que esté escribiendo una aplicación CRUD simple, Spock es la solución perfecta. ¿Usas corutinas y reflexiones? Entonces mejor mira a Spek o Kotest .

El material no pretende ser completo, así que si ha encontrado otras dificultades al usar Spock y Kotlin, escriba los comentarios.

Enlaces útiles



All Articles