Compartilhando Datasets e Builders no Gradle 9

Ao final desta aula, você será capaz de:

  • Saber como empacotar e publicar nossos datasets do DbUnit e builders de teste em um repositório Maven utilizando o Gradle.
  • Utilizar os datasets do DbUnit e builders publicados a partir de outro módulo Gradle no projeto.

Esta aula assume que:

  • Você já compreende por que e como compartilhar fixtures entre módulos em uma arquitetura corporativa (Compartilhando Fixtures Entre Módulos com java-test-fixtures).
  • Você sabe escrever testes de integração para aplicações Spring Boot utilizando Kotlin 2.4.21, Gradle 9.8.2, Spring Boot 4.1.2 e JUnit Jupiter 6.0.3.

Obtenha o código-fonte no GitHub

Publicando Nossos Datasets de Teste e Builders em um Repositório Maven

Quando queremos utilizar os mesmos datasets do DbUnit (e seus respectivos builders e constantes de teste) a partir de múltiplos módulos Gradle, o primeiro passo consiste em empacotar e publicar esses recursos em um repositório Maven (seja o repositório local ou um repositório remoto corporativo).

Podemos publicar nossos datasets e builders seguindo estes passos:

Primeiro, precisamos criar um novo projeto ou submódulo Gradle dedicado a conter nossos dados de teste (por exemplo, test-data-sets). Como queremos utilizar o layout de diretórios padrão de um projeto JVM, aplicamos o plugin JVM no arquivo build.gradle.kts.

No Gradle clássico com Groovy DSL, utilizava-se a instrução plugins { id 'java' }. Na stack moderna com Gradle 9.8.2 e Kotlin 2.4.21, configuramos nosso arquivo build.gradle.kts da seguinte forma:

1
2
3
plugins {
    kotlin("jvm") version "2.4.21"
}

Segundo, precisamos colocar nossos arquivos de dataset do DbUnit no diretório src/main/resources. Se também estivermos compartilhando builders de dados de teste (padrão Object Mother ou Test Data Builder) e constantes de teste (como IDs ou emails fixos), colocamos essas classes no diretório src/main/kotlin.

Essa organização garante que tanto os arquivos XML de dados quanto as classes Kotlin compiladas sejam empacotados diretamente dentro do arquivo JAR gerado.

A estrutura de diretórios a seguir ilustra como os datasets do DbUnit e os builders são organizados no diretório src/main/ da nossa aplicação de exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
test-data-sets/
├── build.gradle.kts
└── src/
    └── main/
        ├── kotlin/
        │   └── com/
        │       └── testwithspring/
        │           └── master/
        │               ├── Users.kt
        │               ├── Tasks.kt
        │               └── TaskTestBuilder.kt
        └── resources/
            └── com/
                └── testwithspring/
                    └── master/
                        ├── no-tasks-and-tags.xml
                        ├── tasks.xml
                        └── users.xml

Nessa estrutura:

  • O diretório resources/com/testwithspring/master/ armazena os datasets XML utilizados pelo DbUnit para pré-popular e verificar o estado do banco de dados relacional (como PostgreSQL).
  • O diretório kotlin/com/testwithspring/master/ contém classes utilitárias, como Users e Tasks, contendo constantes que espelham com precisão as chaves primárias e atributos dos registros XML, além de classes como TaskTestBuilder para construir instâncias de domínio em memória.

Terceiro, precisamos garantir que podemos publicar nosso arquivo JAR em um repositório Maven. Podemos fazer isso seguindo estes passos:

  1. Aplicamos o plugin maven-publish (e opcionalmente java-test-fixtures se quisermos suporte nativo a variantes de teste no Gradle). O plugin maven-publish fornece as tarefas necessárias para publicar artefatos de compilação em repositórios no formato Maven.
  2. Configuramos o groupId, artifactId e version do arquivo JAR publicado dentro do bloco publishing. Além disso, indicamos que a publicação deve empacotar os componentes gerados pelo plugin JVM (from(components["java"])).

Depois de realizarmos as alterações necessárias no nosso arquivo build.gradle.kts, ele se parece com o seguinte:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
plugins {
    kotlin("jvm") version "2.4.21"
    `maven-publish`
    `java-test-fixtures`
}

group = "com.testwithspring.master"
version = "0.1"

publishing {
    publications {
        create<MavenPublication>("mavenJava") {
            groupId = "com.testwithspring.master"
            artifactId = "test-data-sets"
            version = "0.1"

            from(components["java"])
        }
    }
}

Quarto, precisamos publicar nosso arquivo JAR no repositório Maven. Como este é um exemplo prático local, publicaremos nosso arquivo JAR no nosso repositório Maven local (~/.m2/repository). Podemos realizar essa publicação executando o seguinte comando no terminal:

1
./gradlew clean publishToMavenLocal

Nota: Se você estiver trabalhando em um projeto de software corporativo no mundo real, você deve publicar o arquivo JAR em um repositório Maven privado (como Nexus, Artifactory, AWS CodeArtifact ou GitHub Packages) acessível pela infraestrutura da sua organização. O benefício dessa abordagem é que você não precisa executar publishToMavenLocal manualmente na máquina de cada desenvolvedor toda vez que alguém fizer alterações nos dados de teste ou builders. Em vez disso, basta atualizar o número da versão da dependência nos módulos consumidores.

Leituras Adicionais:

A seguir, veremos como podemos utilizar nossos datasets do DbUnit e builders a partir de outro módulo Gradle consumidor.

Utilizando Nossos Datasets de Teste e Builders a Partir de Outro Módulo Gradle

Quando queremos utilizar nossos datasets do DbUnit e builders compartilhados a partir de outro módulo Gradle, devemos seguir estes passos:

Primeiro, precisamos garantir que o Gradle consulte o repositório Maven local (ou o repositório remoto corporativo) ao resolver as dependências do nosso build. Podemos fazer isso adicionando o repositório correspondente ao bloco repositories no arquivo build.gradle.kts:

1
2
3
4
repositories {
    mavenCentral()
    mavenLocal()
}

Segundo, precisamos garantir que nossos datasets do DbUnit e builders estejam disponíveis no classpath de execução dos nossos testes.

No Gradle clássico com Groovy DSL, costumava-se configurar a dependência na configuração legada integrationTestRuntimeOnly('com.testwithspring.master:test-data-sets:0.1').

Na stack moderna com Gradle 9.8.2 (Kotlin DSL), podemos declarar a dependência de duas formas, dependendo de como estruturamos a suíte de testes:

  1. Configuração padrão de dependências de teste:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dependencies {
    // Se precisarmos apenas dos arquivos XML em tempo de execução para o DbUnit:
    testRuntimeOnly("com.testwithspring.master:test-data-sets:0.1")

    // Se também utilizarmos as classes Kotlin de constantes (Users, Tasks) ou builders (TaskTestBuilder):
    testImplementation("com.testwithspring.master:test-data-sets:0.1")

    // Se estivermos em um multi-projeto Gradle e quisermos consumir fixtures locais sem publicar:
    // testImplementation(testFixtures(project(":test-data-sets")))
}
  1. Configuração com a moderna API de JVM Test Suites do Gradle 9:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            dependencies {
                implementation("com.testwithspring.master:test-data-sets:0.1")
            }
        }
    }
}

Diferença entre escopos: Se o módulo test-data-sets contiver apenas arquivos XML/YAML estáticos consumidos pelo DbUnit via caminho de recurso (resource path), a configuração testRuntimeOnly é suficiente. No entanto, como também compartilhamos classes de teste (como builders fluentes e objetos com constantes de teste), devemos utilizar testImplementation (ou implementation dentro do JvmTestSuite), permitindo que essas classes sejam importadas e compiladas no código dos nossos testes.

Terceiro, como os datasets compartilhados do DbUnit e as classes auxiliares são encontrados diretamente no classpath de testes, podemos utilizá-los exatamente da mesma forma que utilizaríamos datasets e builders locais.

Por exemplo, vamos configurar os dados de teste utilizados por um teste de integração que valida se o objeto TaskRepository funciona conforme o esperado ao criar uma nova tarefa. Depois de configurarmos os dados de teste utilizando Spring Boot 4.1.2, Kotlin 2.4.21 e JUnit Jupiter 6.0.3, o código-fonte da nossa classe de teste se parece com o seguinte:

 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
package com.testwithspring.master.task

import com.github.springtestdbunit.annotation.DatabaseSetup
import com.github.springtestdbunit.annotation.DbUnitConfiguration
import com.github.springtestdbunit.annotation.ExpectedDatabase
import com.github.springtestdbunit.assertion.DatabaseAssertionMode
import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
import com.testwithspring.master.DatabaseIntegrationTest
import com.testwithspring.master.Users
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
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.jdbc.core.namedparam.NamedParameterJdbcTemplate

@DatabaseIntegrationTest
@ExtendWith(SoftAssertionsExtension::class)
@DatabaseSetup(
    value = [
        "/com/testwithspring/master/users.xml",
        "/com/testwithspring/master/no-tasks-and-tags.xml"
    ]
)
@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader::class)
@DisplayName("Criar uma nova tarefa")
internal class CreateNewTaskTest(
    @Autowired private val jdbcTemplate: NamedParameterJdbcTemplate,
    @Autowired private val repository: TaskRepository
) {

    @Test
    @ExpectedDatabase(
        value = "/com/testwithspring/master/tasks.xml",
        assertionMode = DatabaseAssertionMode.NON_STRICT
    )
    fun `deve persistir nova tarefa no banco de dados com sucesso`() {
        val newTask = Task(
            title = "Escrever testes de integração com datasets compartilhados",
            description = "Validar a persistência com DbUnit e Gradle 9",
            status = TaskStatus.OPEN,
            userId = Users.JOHN_DOE_ID
        )

        val createdTask = repository.create(newTask)

        assertThat(createdTask.id).isNotNull()
        assertThat(createdTask.title).isEqualTo(newTask.title)
    }
}

Observe que:

  • Os arquivos /com/testwithspring/master/users.xml e /com/testwithspring/master/no-tasks-and-tags.xml são carregados da raiz do classpath, onde foram colocados pelo artefato JAR compartilhado.
  • A constante Users.JOHN_DOE_ID é importada diretamente da biblioteca compartilhada test-data-sets, garantindo que o ID do usuário utilizado no teste corresponda com absoluta fidelidade ao registro inserido no banco de dados pelo dataset XML.
  • Se quisermos instanciar modelos de teste complexos, podemos também usar builders fluentes importados da biblioteca de fixtures compartilhada:
1
2
3
4
5
6
val task = TaskTestBuilder()
    .withTitle("Escrever testes de integração com datasets compartilhados")
    .withDescription("Validar a persistência com DbUnit e Gradle 9")
    .withStatus(TaskStatus.OPEN)
    .withUserId(Users.JOHN_DOE_ID)
    .build()

Boas Práticas ao Compartilhar Datasets e Builders no Gradle

Para garantir que a manutenção de testes distribuídos entre múltiplos módulos permaneça simples e previsível, adote as seguintes práticas recomendadas:

  1. Versionamento Semântico Rigoroso: Toda vez que você alterar um dataset ou um builder no módulo de dados de teste, incremente o número da versão (por exemplo, de 0.1 para 0.2 ou 1.0.0). Isso impede que modificações nos dados de teste quebrem inesperadamente testes em outros módulos que ainda dependem do estado anterior.
  2. Co-localização de Constantes e Builders: Mantenha as constantes de IDs (Users.JOHN_DOE_ID, Tasks.TASK_ID) e classes builders no mesmo módulo que contém os arquivos XML do DbUnit. Colocá-las em src/main/kotlin garante que alterações em campos do schema sejam atualizadas simultaneamente no XML e no código Kotlin em um único commit.
  3. Sincronização com as Migrações de Banco de Dados: A evolução dos datasets deve acompanhar as migrações estruturais do banco de dados (gerenciadas pelo Flyway ou Liquibase). Se uma nova coluna obrigatória for adicionada a uma tabela, atualize os datasets XML correspondentes imediatamente.
  4. Alinhamento de Versão: Inclua o identificador da versão do modelo de dados no artefato de dados de teste (por exemplo, test-data-sets:2.1.0), tornando explícito qual versão de schema do banco de dados aquele conjunto de dados reflete.
  5. Não Poluir Datasets Compartilhados: Datasets compartilhados devem conter apenas dados canônicos e comuns a vários módulos. Cenários ou casos de borda específicos de um único módulo devem ser mantidos como datasets locais dentro do diretório src/test/resources do respectivo módulo consumidor.
  6. Aproveitamento de Recursos do Gradle 9: Em repositórios multi-módulo gerenciados em monorepo, utilize a convenção java-test-fixtures (testImplementation(testFixtures(project(":core")))) para compartilhamento rápido sem necessidade de publicar em mavenLocal(), reservando a publicação via maven-publish para projetos distribuídos entre repositórios git separados.

Resumo

Nesta aula, aprendemos que:

  • Podemos publicar nossos artefatos de dados de teste (datasets XML, builders e constantes em Kotlin) em um repositório Maven utilizando o plugin maven-publish no Gradle 9.8.2.
  • Como os datasets e builders compartilhados são empacotados em um arquivo JAR e adicionados ao classpath de testes do módulo consumidor, podemos utilizá-los com as anotações do DbUnit (@DatabaseSetup, @ExpectedDatabase) exatamente como se fossem arquivos locais.
  • No ecossistema moderno do Gradle 9, a combinação de build.gradle.kts, JvmTestSuite e java-test-fixtures oferece flexibilidade tanto para publicação externa em repositórios corporativos quanto para consumo direto em monorepos.

Obtenha o código-fonte no GitHub