Estilos de Especificação do Kotest (DescribeSpec, BehaviorSpec) e Ciclo de Vida
Depois de concluirmos esta aula, nós:
- Saberemos como criar classes de especificação no Kotest 6 e escolher o estilo de especificação mais adequado (
DescribeSpec,BehaviorSpec,ShouldSpec). - Conheceremos a estrutura interna completa de uma classe de especificação.
- Compreenderemos a diferença entre propriedades isoladas por teste e propriedades compartilhadas, e como configurar o
IsolationModedo Kotest. - Saberemos como utilizar os métodos/callbacks de fixture (
beforeSpec,beforeEach,afterEach,afterSpec) e sua ordem exata de invocação.
1. Criando Nossa Primeira Especificação no Kotest
A classe que contém nossos casos de teste é chamada de classe de especificação (specification class). No Spock, toda classe estendia uma única classe base (Specification). No Kotest 6, estendemos a classe base correspondente ao estilo de especificação que preferirmos para o nosso projeto, passando uma lambda para o construtor (ou sobrescrevendo o bloco init):
- Criamos uma nova classe Kotlin em
src/test/kotlin. - Estendemos um dos estilos do pacote
io.kotest.core.spec.style.*— os três mais utilizados em aplicações Spring Boot são:DescribeSpec: inspirado em RSpec/Jasmine/Mocha, organiza os testes em blocosdescribe("...")eit("...").BehaviorSpec: inspirado em BDD clássico, organiza os testes em blocosGiven("..."),When("...")eThen("...").ShouldSpec: enxuto e direto, organiza os testes em blocoscontext("...")eshould("...").
- Opcionalmente, configuramos a tag da especificação com
@Tags("UnitTest").
Veja como fica uma classe de especificação simples recém-criada:
|
|
[!NOTE] Duas observações importantes antes de prosseguirmos:
- A anotação
@Tags("UnitTest")só é necessária se você filtrar a execução por tags no seu build. Se você já separa seus testes usando diretórios dedicados dojvm-test-suite(src/test/kotlin,src/integrationTest/kotlin,src/endToEndTest/kotlin), ela é totalmente opcional.- Seguimos a convenção de nomenclatura de adicionar o sufixo
Specao nome do sistema sob especificação (por exemplo,MessageService->MessageServiceSpec).
Nossa especificação ainda está vazia. Antes de adicionarmos testes a ela, vamos conhecer as partes que compõem a estrutura de uma especificação do Kotest.
2. A Estrutura de uma Especificação do Kotest
Uma classe de especificação do Kotest pode conter os seguintes blocos:
- Constantes (
companion object): constantes estáticas (private const val) úteis para dar nomes semânticos a valores mágicos. - Propriedades / Campos (
val/var): armazenam objetos que pertencem à fixture da especificação (como o sistema sob especificação e seus colaboradores/dublês). - Callbacks de Fixture (
beforeSpec,beforeEach,afterEach,afterSpec): responsáveis por inicializar recursos ou configurar o sistema sob especificação antes da execução dos testes e limpá-los depois. - Blocos de Especificação (Feature Blocks): descrevem e verificam o comportamento esperado do sistema sob especificação (
describe/it,Given/When/Thenoucontext/should). - Funções Auxiliares (Helper Functions): métodos privados declarados dentro da especificação para evitar duplicação de código nos testes.
O exemplo abaixo ilustra o esqueleto estrutural de uma especificação no Kotest:
|
|
Vamos agora examinar em detalhes como funcionam as constantes, as propriedades isoladas e as propriedades compartilhadas!
3. Adicionando Constantes, Propriedades Isoladas e Propriedades Compartilhadas
No Spock Framework, os campos normais eram recriados para cada teste, enquanto campos anotados com @Shared eram compartilhados entre todos os testes.
No Kotest 6, você tem controle explícito e transparente sobre isso por meio do IsolationMode:
IsolationMode.InstancePerLeaf(ouIsolationMode.InstancePerTest): o Kotest cria uma nova instância fresca da classe de especificação para cada caso de teste folha executado! Isso significa que qualquer variável declarada no corpo da especificação (val messageService = MessageService()) não é compartilhada entre os testes — cada teste recebe seu próprio objeto isolado!- Objetos Compartilhados (
companion objectouIsolationMode.SingleInstance): se criar um objeto for muito custoso (como um contêiner ou cliente pesado) e quisermos compartilhá-lo entre todos os testes da especificação, podemos declará-lo nocompanion object(ou inicializar dentro de umbeforeSpec).
Vamos demonstrar na prática a diferença entre um objeto isolado por teste e um objeto compartilhado criando a classe FieldExampleSpec em três passos:
- Adicionamos uma constante
MESSAGE = "Hello World!"nocompanion object. - Configuramos
isolationMode = IsolationMode.InstancePerLeafe declaramosval messageService = MessageService(). Como a instância é isolada por teste folha, cada teste receberá sua própria instância deMessageService. - Declaramos
val sharedObject = Any()dentro docompanion objectpara que ambos os testes recebam exatamente a mesma referência em memória.
|
|
Quando executamos FieldExampleSpec, vemos as seguintes linhas impressas em System.out:
|
|
Observe os hashes de memória na saída acima:
- O objeto armazenado em
messageServicepossui hashes diferentes (@6b419davs@309e345f), provando que não é compartilhado entre os testes! - O objeto armazenado em
sharedObjectpossui o mesmo hash (@60dcc9fe), provando que é compartilhado entre todos os testes da especificação.
[!TIP] Você também pode definir
IsolationMode.InstancePerLeafglobalmente para todo o seu projeto criando uma classeProjectConfig : AbstractProjectConfig()comoverride val isolationMode = IsolationMode.InstancePerLeaf. Assim, você não precisa repetir essa linha em cada especificação!
4. Utilizando Callbacks de Fixture (beforeSpec, beforeEach, afterEach, afterSpec)
Os callbacks de fixture são usados para configurar e limpar o sistema sob especificação antes e depois dos testes. Todos eles são opcionais e suportam funções suspensas (suspend) do Kotlin nativamente!
Uma especificação do Kotest suporta quatro callbacks principais de ciclo de vida (equivalentes diretos aos métodos de fixture do Spock):
beforeSpec { ... }(equivalente aosetupSpec()): é invocado uma única vez antes que o primeiro teste da especificação seja executado. Ideal para configurações pesadas compartilhadas.beforeEach { ... }(equivalente aosetup()): é invocado antes de cada caso de teste folha. Usamos esse bloco para configurar o sistema sob especificação ou resetar dublês de teste (clearMocks(...)).afterEach { ... }(equivalente aocleanup()): é invocado após a conclusão de cada caso de teste folha. Usamos para limpar qualquer configuração feita nobeforeEach.afterSpec { ... }(equivalente aocleanupSpec()): é invocado uma única vez depois que todos os testes da especificação terminaram. Usamos para liberar recursos alocados nobeforeSpec.
Vamos adicionar todos os quatro callbacks de fixture e dois casos de teste na nossa classe FixtureMethodExampleSpec:
|
|
Quando executamos FixtureMethodExampleSpec, observamos a seguinte saída no console:
|
|
Essa saída comprova que os callbacks e os testes da nossa especificação são invocados exatamente nesta ordem:
beforeSpecbeforeEach- Primeiro teste (
"Get message one") afterEachbeforeEach- Segundo teste (
"Get message two") afterEachafterSpec
Resumo
Nesta aula, aprendemos cinco conceitos fundamentais sobre as especificações do Kotest:
- Toda classe de especificação do Kotest estende uma classe de estilo (
DescribeSpec,BehaviorSpec,ShouldSpec, etc.). - Uma especificação pode conter constantes, propriedades de instância, callbacks de fixture, blocos de especificação e funções auxiliares.
- Devemos preferir propriedades isoladas por teste (via
IsolationMode.InstancePerLeafou reatribuição nobeforeEach) para garantir que um teste nunca polua o estado do próximo. - Devemos usar objetos compartilhados (
companion object/beforeSpec) apenas quando a criação do objeto for cara ou quando o compartilhamento for intencional. - Inicializamos e limpamos o sistema sob especificação de forma determinística usando
beforeSpec,beforeEach,afterEacheafterSpec.