Executando Verificação de Contratos com JUnit 6

Ao concluirmos este webinar, seremos capazes de:

  • Criar e configurar um projeto Gradle (Kotlin DSL) que utiliza o Spring Cloud Contract 5.0 e o JUnit Jupiter 6.0.3.
  • Compreender como utilizar o Spring Cloud Contract em conjunto com o JUnit 6 (JUnit Platform / Jupiter) ao implementarmos serviços REST produtores (producers) e consumidores (consumers) em Kotlin.

Este webinar assume que:

Antes de começarmos, vale destacar o contexto histórico e a evolução da stack: quando o suporte à plataforma JUnit Jupiter foi originalmente introduzido no ecossistema Spring Cloud Contract (na época da versão 2.1.0.M2), era necessário recorrer a repositórios Milestone do Spring porque a linha estável antiga suportava apenas JUnit 4 por padrão. Na stack moderna com Spring Boot 4.1.2, Spring Cloud Contract 5.0, Kotlin 2.4.21 e Gradle 9.8.2, o suporte à plataforma JUnit Jupiter (JUnit 6.0.3) é nativo, mas ainda precisamos configurar explicitamente a DSL do plugin, a plataforma de testes do Gradle e o modo de geração TestFramework.JUNIT5 (identificador usado pelo gerador do Spring Cloud Contract para toda a arquitetura JUnit Jupiter 5/6).

Obtenha o código-fonte de referência no GitHub

Modificando o Serviço REST Produtor

Quando queremos utilizar o JUnit Jupiter 6 no projeto do nosso serviço REST produtor (producer), precisamos modificar o nosso arquivo de build (build.gradle.kts) e a classe base que é estendida pelas classes de teste reais geradas pelo plugin Gradle do Spring Cloud Contract.

Vamos começar modificando o nosso arquivo de build.

Modificando o Nosso Arquivo de Build (build.gradle.kts)

Podemos realizar as alterações necessárias no nosso arquivo build.gradle.kts seguindo estes passos:

Primeiro, precisamos garantir que o Gradle configure os repositórios necessários (como o mavenCentral() e, caso você esteja testando versões pré-lançamento ou milestones do ecossistema Spring Cloud, o repositório https://repo.spring.io/milestone) tanto para a resolução das dependências do nosso serviço REST produtor quanto para os plugins utilizados pelo build do Gradle.

Podemos fazer isso adicionando o seguinte trecho ao nosso arquivo settings.gradle.kts (para plugins) e build.gradle.kts (para dependências):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Em settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        maven { url = uri("https://repo.spring.io/milestone") }
    }
}

// Em build.gradle.kts
repositories {
    mavenCentral()
    maven { url = uri("https://repo.spring.io/milestone") }
}

Segundo, precisamos definir a versão do BOM (Bill of Materials) do Spring Cloud Contract (spring-cloud-contract-dependencies) utilizado pelo nosso projeto Gradle. Isso garante que o nosso projeto utilize versões de dependências compatíveis entre si e alinhadas ao Spring Boot 4.1.2.

Podemos realizar a alteração necessária configurando o bloco dependencyManagement (ou platform) para a versão 5.0.0. Após fazermos essa alteração, a parte relevante do nosso arquivo build.gradle.kts ficará da seguinte forma:

1
2
3
4
5
dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-contract-dependencies:5.0.0")
    }
}

Terceiro, precisamos declarar as dependências do JUnit Jupiter 6 no nosso arquivo build.gradle.kts. Fazemos isso adicionando as dependências junit-jupiter-api e junit-jupiter-engine (na versão 6.0.3), além do lançador da plataforma, ao bloco dependencies do nosso arquivo build.gradle.kts.

Em outras palavras, precisamos adicionar o seguinte trecho à seção dependencies do nosso arquivo de build:

1
2
3
4
5
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.3")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Quarto, como utilizamos o JUnit Jupiter 6, precisamos garantir que o Gradle execute os nossos testes unitários e de contrato utilizando a JUnit Platform nativa na tarefa test (no Maven clássico, isso equivalia a atualizar o maven-surefire-plugin para 2.22.1+ para evitar a necessidade de configurar o antigo junit-platform-surefire-provider).

Podemos configurar a execução nativa da JUnit Platform adicionando o seguinte bloco tasks.withType<Test>() ao nosso arquivo build.gradle.kts:

1
2
3
tasks.withType<Test>().configureEach {
    useJUnitPlatform()
}

Quinto, precisamos realizar as seguintes alterações na configuração do plugin do Spring Cloud Contract (org.springframework.cloud.contract no Gradle):

  1. Definir a versão do plugin do Spring Cloud Contract para 5.0.0 no bloco plugins.
  2. Garantir que o plugin do Spring Cloud Contract gere classes de teste que utilizem a arquitetura JUnit Jupiter (TestFramework.JUNIT5, que gera testes anotados com @ org.junit.jupiter.api.Test compatíveis com JUnit 5 e JUnit 6).

Após realizarmos as alterações necessárias, a configuração do plugin do Spring Cloud Contract no nosso build.gradle.kts ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.springframework.cloud.contract.verifier.config.TestFramework

plugins {
    kotlin("jvm") version "2.4.21"
    kotlin("plugin.spring") version "2.4.21"
    id("org.springframework.boot") version "4.1.2"
    id("io.spring.dependency-management") version "1.1.7"
    id("org.springframework.cloud.contract") version "5.0.0"
}

contracts {
    baseClassForTests.set("com.testwithspring.master.webinar.BaseTest")
    testFramework.set(TestFramework.JUNIT5)
}

Já realizamos todas as alterações necessárias no nosso build Gradle. A seguir, modificaremos a classe base dos nossos testes de contrato.

Modificando a Nossa Classe Base de Teste

Como nos lembramos da Parte 1, a classe base dos nossos testes de verificação de contrato configura o sistema sob teste (System Under Test) utilizando um método de preparação (setup) que é executado antes de cada método de teste gerado. É por isso que a única alteração estrutural que precisamos fazer na migração do JUnit 4 para o JUnit Jupiter 6 é substituir a antiga anotação @Before pela anotação @BeforeEach (org.junit.jupiter.api.BeforeEach).

Após realizarmos essa alteração, o código-fonte da classe BaseTest em Kotlin ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package com.testwithspring.master.webinar

import io.restassured.module.mockmvc.RestAssuredMockMvc
import org.junit.jupiter.api.BeforeEach
import org.mockito.BDDMockito.given
import org.mockito.Mockito.mock

abstract class BaseTest {

    @BeforeEach
    fun setup() {
        val service = mock(TaskService::class.java)

        RestAssuredMockMvc.standaloneSetup(TaskController(service))
        given(service.findById(1L))
            .willReturn(Task(1L, "Write our first contract"))
    }
}

Concluímos as alterações necessárias no nosso serviço REST produtor. Vamos avançar e descobrir como podemos aplicar as alterações equivalentes ao nosso serviço REST consumidor.

Modificando o Serviço REST Consumidor

Quando queremos utilizar o JUnit Jupiter 6 no projeto do nosso serviço REST consumidor (consumer), precisamos modificar o nosso arquivo de build (build.gradle.kts) e as classes de teste que garantem que o sistema sob teste esteja funcionando conforme o esperado quando o contrato entre o serviço consumidor e o serviço produtor não foi alterado.

Vamos começar modificando o nosso arquivo de build.

Modificando o Nosso Arquivo de Build (build.gradle.kts)

Podemos realizar as alterações necessárias no nosso arquivo build.gradle.kts do consumidor seguindo estes passos:

Primeiro, precisamos garantir que o Gradle configure os repositórios necessários (mavenCentral() e, quando aplicável para dependências milestone do Spring, https://repo.spring.io/milestone) ao baixar as dependências do nosso serviço REST consumidor.

Podemos fazer isso adicionando o seguinte trecho ao nosso arquivo build.gradle.kts:

1
2
3
4
repositories {
    mavenCentral()
    maven { url = uri("https://repo.spring.io/milestone") }
}

Segundo, precisamos definir a versão do BOM do Spring Cloud Contract que é utilizada pelo nosso projeto consumidor. Isso garante que o Stub Runner e as demais bibliotecas do projeto utilizem as versões corretas de dependências.

Podemos realizar a alteração necessária definindo a versão do BOM spring-cloud-contract-dependencies para 5.0.0. Após fazermos essa alteração, a parte relevante do nosso arquivo build.gradle.kts ficará da seguinte forma:

1
2
3
4
5
dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-contract-dependencies:5.0.0")
    }
}

Terceiro, precisamos declarar as dependências do JUnit Jupiter 6 no nosso arquivo build.gradle.kts. Fazemos isso adicionando as dependências junit-jupiter-api e junit-jupiter-engine (versão 6.0.3) à seção dependencies do nosso arquivo build.gradle.kts.

Em outras palavras, precisamos adicionar o seguinte trecho à seção dependencies do nosso arquivo de build:

1
2
3
4
5
6
dependencies {
    testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
    testImplementation("org.junit.jupiter:junit-jupiter-api:6.0.3")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.3")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Quarto, como utilizamos o JUnit Jupiter 6, precisamos garantir que o Gradle execute os nossos testes utilizando o suporte nativo à JUnit Platform (assim como no Maven Surefire 2.22.1+, não precisamos configurar provedores externos manuais).

Podemos configurar a execução na tarefa test adicionando o seguinte trecho ao nosso arquivo build.gradle.kts:

1
2
3
tasks.withType<Test>().configureEach {
    useJUnitPlatform()
}

Já realizamos as alterações necessárias na configuração de build do nosso projeto consumidor. A seguir, veremos como podemos modificar as nossas classes de teste.

Modificando as Nossas Classes de Teste

Como nos lembramos da Parte 2, o projeto do nosso serviço REST consumidor possui duas classes de teste. Esses testes nos ajudam a garantir que:

  • O contrato fornecido pelo nosso serviço REST produtor não sofreu quebras de compatibilidade.
  • O nosso serviço REST consumidor está funcionando conforme o esperado quando a tarefa (task) solicitada é encontrada.

Podemos migrar esses testes do JUnit 4 para o JUnit Jupiter 6 seguindo estes passos:

Primeiro, precisamos realizar as seguintes alterações na classe TaskContractTest:

  1. Utilizar a extensão SpringExtension (@ExtendWith(SpringExtension::class)) em vez do antigo runner SpringRunner (@RunWith(SpringRunner.class)). (Nota: no Spring Boot moderno, @SpringBootTest já é meta-anotada com @ExtendWith(SpringExtension::class), mas declará-la explicitamente deixa evidente a transição arquitetural do modelo de Runners do JUnit 4 para o modelo de Extensions do JUnit Jupiter).
  2. Substituir a anotação @Before pela anotação @BeforeEach.
  3. Substituir a anotação org.junit.Test pela anotação org.junit.jupiter.api.Test.
  4. Configurar os nomes de exibição legíveis dos nossos métodos de teste utilizando a anotação @DisplayName (ou nomes de funções com crases no Kotlin).

Após realizarmos essas alterações, o código-fonte da classe TaskContractTest ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.testwithspring.master.webinar

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.web.client.RestTemplate

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureStubRunner(
    stubsMode = StubRunnerProperties.StubsMode.LOCAL,
    ids = ["com.testwithspring.master.webinar:contract-rest-service-junit5:0.0.1-SNAPSHOT:stubs:9090"]
)
class TaskContractTest {

    private lateinit var restTemplate: RestTemplate

    @BeforeEach
    fun setup() {
        restTemplate = RestTemplate()
    }

    @Test
    @DisplayName("Should return the HTTP status code 200")
    fun shouldReturnHttpStatusCodeOk() {
        val taskResponse: ResponseEntity<Task> = restTemplate.getForEntity(
            "http://localhost:9090/api/task/1",
            Task::class.java
        )
        assertThat(taskResponse.statusCode).isEqualTo(HttpStatus.OK)
    }

    @Test
    @DisplayName("Should return a task that has the correct id")
    fun shouldReturnTaskWithCorrectId() {
        val taskResponse: ResponseEntity<Task> = restTemplate.getForEntity(
            "http://localhost:9090/api/task/1",
            Task::class.java
        )

        val task = requireNotNull(taskResponse.body)
        assertThat(task.id).isEqualByComparingTo(1L)
    }

    @Test
    @DisplayName("Should return a task that has the correct title")
    fun shouldReturnTaskWithCorrectTitle() {
        val taskResponse: ResponseEntity<Task> = restTemplate.getForEntity(
            "http://localhost:9090/api/task/1",
            Task::class.java
        )

        val task = requireNotNull(taskResponse.body)
        assertThat(task.title).isEqualTo("Write our first contract")
    }
}

Segundo, precisamos realizar as seguintes alterações na classe FindTaskByIdWhenTaskIsFoundTest:

  1. Utilizar a classe SpringExtension (@ExtendWith(SpringExtension::class)) em vez da classe SpringRunner.
  2. Substituir a anotação org.junit.Test pela anotação org.junit.jupiter.api.Test.
  3. Configurar os nomes de exibição dos nossos métodos de teste utilizando a anotação @DisplayName.

Após realizarmos essas alterações, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.testwithspring.master.webinar

import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.http.MediaType
import org.springframework.test.context.junit.jupiter.SpringExtension
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@ExtendWith(SpringExtension::class)
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureStubRunner(
    stubsMode = StubRunnerProperties.StubsMode.LOCAL,
    ids = ["com.testwithspring.master.webinar:contract-rest-service-junit5:0.0.1-SNAPSHOT:stubs:8080"]
)
class FindTaskByIdWhenTaskIsFoundTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    @DisplayName("Should return the HTTP status code OK")
    fun shouldReturnHttpStatusCodeOk() {
        mockMvc.perform(get("/api/task/{id}", 1L))
            .andExpect(status().isOk)
    }

    @Test
    @DisplayName("Should return the found task as JSON")
    fun shouldReturnFoundTaskAsJson() {
        mockMvc.perform(get("/api/task/{id}", 1L))
            .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    }

    @Test
    @DisplayName("Should return the information of the correct task")
    fun shouldReturnInformationOfCorrectTask() {
        mockMvc.perform(get("/api/task/{id}", 1L))
            .andExpect(jsonPath("$.id", `is`(1)))
            .andExpect(jsonPath("$.title", `is`("Write our first contract")))
    }
}

Concluímos todas as alterações necessárias no projeto do nosso serviço REST consumidor. Vamos resumir o que aprendemos neste webinar.

Resumo

Este webinar nos ensinou quatro pontos essenciais:

  • Precisamos alinhar a versão do BOM e do plugin do Spring Cloud Contract (5.0.0 no ecossistema Spring Boot 4.1.2; historicamente introduzido na versão 2.1.0.M2) para contarmos com suporte completo à plataforma JUnit Jupiter (JUnit 5/6).
  • Se quisermos utilizar o JUnit Jupiter 6 ao escrevermos o nosso serviço REST produtor, precisamos alterar a configuração do plugin do Spring Cloud Contract no nosso arquivo de build. Mais especificamente, devemos definir a opção de configuração testFramework como TestFramework.JUNIT5 (que instrui o gerador de código a emitir testes baseados em org.junit.jupiter.api.*).
  • Precisamos executar os nossos testes habilitando o suporte nativo à JUnit Platform na ferramenta de build (useJUnitPlatform() no Gradle 9.8.2, equivalente ao suporte nativo do Maven Surefire 2.22.1+).
  • Podemos migrar nossas classes base e classes de teste de consumidor do JUnit 4 para o JUnit Jupiter 6 substituindo as anotações antigas do JUnit 4 (@RunWith, @Before, @org.junit.Test) pelas novas anotações providas pelo JUnit Jupiter (@ExtendWith(SpringExtension::class), @BeforeEach, @org.junit.jupiter.api.Test e @DisplayName).

Obtenha o código-fonte de referência no GitHub