Testando em Kotlin com Spock

O objetivo do artigo é mostrar quais dificuldades surgem ao usar o Spock com o Kotlin, quais são as maneiras de resolvê-las e responder à pergunta se vale a pena usar o Spock se você estiver desenvolvendo no Kotlin. Detalhes sob o corte.

Eu trabalho para uma empresa que pratica programação extrema . Um dos principais métodos de programação extrema que usamos em nosso trabalho diário é o TDD (desenvolvimento orientado a testes). Isso significa que, antes de alterar o código, escrevemos um teste que cobre a alteração desejada. Assim, escrevemos testes regularmente e temos cobertura de código com testes próximos a 100%. Isso requer certos requisitos para a escolha de uma estrutura de teste: uma coisa é escrever testes uma vez por semana, e outra é fazê-lo todos os dias.

Estamos desenvolvendo o Kotlin e, em algum momento, escolhemos Spock como a estrutura principal. Aproximadamente 6 meses se passaram desde aquele momento, uma sensação de euforia e novidade se passou. Portanto, o artigo é uma espécie de retrospectiva na qual tentarei explicar quais dificuldades encontramos durante esse período e como as resolvemos.

Por que exatamente Spock?




Primeiro, você precisa descobrir quais estruturas permitem que o Kotlin seja testado e quais vantagens o Spock oferece em comparação com elas.

Uma das vantagens do Kotlin é sua compatibilidade com o Java, que permite usar qualquer estrutura Java para teste, como Junit , TestNG , Spock , etc. Ao mesmo tempo, existem estruturas projetadas especificamente para o Kotlin, como Spek e Kotest . Por que escolhemos Spock?

Eu destacaria as seguintes vantagens:

  • Primeiro, Spock é escrito em Groovy. Bom ou ruim - julgue por si mesmo. Sobre o Groovy, você pode ler o artigo aqui . Groovy pessoalmente me atrai para a sintaxe lacônica, a presença de digitação dinâmica, a sintaxe interna para listas, matrizes regulares e associativas e conversões de tipos completamente loucas.
  • Em segundo lugar, Spock já contém uma estrutura simulada (MockingApi) e uma biblioteca de asserções;
  • Spock também é ótimo para escrever testes parametrizados:

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

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

Essa é uma comparação muito superficial (se é que pode ser chamada de comparação), mas essas são basicamente as razões pelas quais escolhemos Spock. Naturalmente, nas semanas e meses seguintes, encontramos algumas dificuldades.

Problemas encontrados ao testar o Kotlin com Spock e resolvê-los


Aqui, faço imediatamente uma reserva de que os problemas descritos abaixo não são exclusivos do Spock, eles serão relevantes para qualquer estrutura Java e estão principalmente associados à compatibilidade com o Kotlin .

1. final por padrão


Problema

Ao contrário do Java, todas as classes Kotlin têm um modificador por padrão final, o que impede descendentes adicionais e substituições de métodos. O problema aqui é que, ao criar objetos simulados de qualquer classe, a maioria das estruturas simuladas tenta criar um objeto proxy, que é apenas um descendente da classe original e substitui seus métodos.

Portanto, se você tiver um serviço:

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

e você tenta criar uma simulação deste serviço no Groovy:

def customerServiceMock = Mock(CustomerService)

então você recebe um erro:
org.spockframework.mock.CannotCreateMockException: Cannot create mock for class CustomerService because Java mocks cannot mock final classes

Decisão:

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

2.


Problema

No Kotlin, os argumentos da função ou do construtor podem ter valores padrão que são usados ​​se o argumento da função não for especificado quando for chamado. A dificuldade é que o bytecode Java perde valores padrão de argumentos e nomes de parâmetros de funções e, portanto, ao chamar uma função ou construtor Kotlin da Spock, todos os valores devem ser especificados explicitamente.

Considere uma classe Customerque tem um construtor com 2 campos obrigatórios emaile namee campos opcionais que têm valores padrão:

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 criar uma instância dessa classe no teste Groovy Spock, você precisará passar valores para todos os argumentos ao construtor:

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

Decisão:


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


Pode ser uma área bastante estreita, mas se você planeja usar a reflexão em seus testes, pense especialmente se deve usar o Groovy. Em nosso projeto, usamos muitas anotações e, para não esquecer de anotar determinadas classes ou certos campos com anotações, escrevemos testes usando reflexão. É aqui que surgem dificuldades.

Problema

Classes e anotações são escritas em Kotlin, lógica relacionada a testes e reflexão no Groovy. Por esse motivo, os testes produzem um hash da API Java Reflection e Kotlin Reflection:

Convertendo classes Java para Kotlin:

def kotlinClass = new KClassImpl(clazz)

Chamar a extensão Kotlin funciona como métodos estáticos:
ReflectJvmMapping.getJavaType(property.returnType)

Código de difícil leitura onde os tipos de Kotlin serão usados:
private static boolean isOfCollectionType(KProperty1<Object, ?> property) {
    // Some logic
}

Obviamente, isso não é algo excessivo ou impossível. A única questão é: não é mais fácil fazer isso com a estrutura de teste Kotlin, onde será possível usar funções de extensão e pura reflexão Kotlin?

4. Corotinas


Se você planeja usar corotinas, esse é outro motivo para pensar se você deseja escolher a estrutura Kotlin para teste.

Problema

Se você criar uma suspendfunção:

suspend fun someSuspendFun() {
    // Some logic
}

, compila da seguinte maneira:

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

onde Continuation é uma classe Java que encapsula a lógica para executar a rotina. É aqui que o problema surge, como criar um objeto dessa classe? Além disso, runBlocking não está disponível no Groovy . E, portanto, geralmente não está muito claro como testar o código com corotinas no Spock.

Sumário


Depois de seis meses de uso do Spock, ele nos trouxe mais bons do que maus e não lamentamos a escolha: os testes são fáceis de ler e manter, escrever testes parametrizados é um prazer. Uma mosca na pomada, no nosso caso, acabou sendo um reflexo, mas é usada em apenas alguns lugares, o mesmo com corotinas.

Ao escolher uma estrutura para testar um projeto que está sendo desenvolvido no Kotlin, você deve pensar cuidadosamente sobre os recursos de linguagem que planeja usar. Caso você esteja escrevendo um aplicativo CRUD simples, Spock é a solução perfeita. Você usa corotinas e reflexão? Então é melhor olhar para Spek ou Kotest .

O material não afirma estar completo; portanto, se você encontrou outras dificuldades ao usar o Spock e o Kotlin - escreva nos comentários.

Links Úteis



All Articles