Estratégia Unificada de Testes no JUnit 6

Depois de concluirmos este webinar bônus, nós:

  • Saberemos criar e configurar um projeto (em Gradle 9 Kotlin DSL ou Maven) que executa testes unitários, de integração e End-to-End utilizando o JUnit 6.
  • Saberemos utilizar tags do JUnit 6 (@Tag) quando estivermos escrevendo e filtrando testes unitários, de integração e End-to-End.

Esta aula assume que:

  • Você sabe executar testes unitários no seu sistema de build.
  • Você sabe executar testes de integração no seu sistema de build.
  • Você sabe executar testes End-to-End no seu sistema de build.
  • Você está familiarizado com os fundamentos do JUnit 6 (Jupiter).

Os Requisitos do Nosso Build

Os requisitos do nosso projeto são:

  • Nossos testes unitários, de integração e End-to-End devem possuir diretórios de código-fonte e de recursos separados:
    • src/test/kotlin e src/test/resources (Testes Unitários)
    • src/integration-test/kotlin e src/integration-test/resources (Testes de Integração)
    • src/end-to-end-test/kotlin e src/end-to-end-test/resources (Testes End-to-End)
  • Apenas os testes unitários devem ser executados por padrão durante o ciclo rápido de desenvolvimento.
  • Devemos ser capazes de executar apenas os testes unitários, apenas os testes de integração ou apenas os testes End-to-End.
  • Os nomes das nossas classes de teste não precisam começar ou terminar com um prefixo/sufixo especial diferente de Test (a separação é feita por diretórios e por @Tag).

Vamos avançar e configurar os diretórios de código-fonte e de recursos dos nossos testes de integração e End-to-End.

Configurando Diretórios e Perfis no Build

Opção A: Configuração Moderna no Gradle 9 (build.gradle.kts)

No Gradle 9, utilizamos o plugin nativo jvm-test-suite combinado com useJUnitPlatform { includeTags(...) }:

 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
plugins {
    kotlin("jvm") version "2.4.21"
    `jvm-test-suite`
}

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            targets {
                all {
                    testTask.configure {
                        useJUnitPlatform {
                            includeTags("UnitTest")
                        }
                    }
                }
            }
        }

        val integrationTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            sources {
                kotlin { setSrcDirs(listOf("src/integration-test/kotlin")) }
                resources { setSrcDirs(listOf("src/integration-test/resources")) }
            }
            dependencies {
                implementation(project())
            }
            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(tasks.named("test"))
                        useJUnitPlatform {
                            includeTags("IntegrationTest")
                        }
                    }
                }
            }
        }

        val endToEndTest by registering(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            sources {
                kotlin { setSrcDirs(listOf("src/end-to-end-test/kotlin")) }
                resources { setSrcDirs(listOf("src/end-to-end-test/resources")) }
            }
            dependencies {
                implementation(project())
            }
            targets {
                all {
                    testTask.configure {
                        shouldRunAfter(tasks.named("integrationTest"))
                        useJUnitPlatform {
                            includeTags("EndToEndTest")
                        }
                    }
                }
            }
        }
    }
}

Opção B: Configuração Equivalente no Maven (pom.xml)

Em projetos Maven, adicionamos os diretórios de código-fonte e recursos adicionais utilizando o Build Helper Maven Plugin e controlamos a execução com perfis (dev, integration-test e end-to-end-test):

Primeiro, configuramos o build-helper-maven-plugin para registrar src/integration-test/kotlin, src/integration-test/resources, src/end-to-end-test/kotlin e src/end-to-end-test/resources:

 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
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>3.6.0</version>
    <executions>
        <execution>
            <id>add-end-to-end-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/end-to-end-test/kotlin</source>
                </sources>
            </configuration>
        </execution>
        <execution>
            <id>add-end-to-end-test-resources</id>
            <phase>generate-test-resources</phase>
            <goals>
                <goal>add-test-resource</goal>
            </goals>
            <configuration>
                <resources>
                    <resource>
                        <filtering>true</filtering>
                        <directory>src/end-to-end-test/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
        <execution>
            <id>add-integration-test-sources</id>
            <phase>generate-test-sources</phase>
            <goals>
                <goal>add-test-source</goal>
            </goals>
            <configuration>
                <sources>
                    <source>src/integration-test/kotlin</source>
                </sources>
            </configuration>
        </execution>
        <execution>
            <id>add-integration-test-resources</id>
            <phase>generate-test-resources</phase>
            <goals>
                <goal>add-test-resource</goal>
            </goals>
            <configuration>
                <resources>
                    <resource>
                        <filtering>true</filtering>
                        <directory>src/integration-test/resources</directory>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

Segundo, adicionamos três propriedades na seção <properties> do nosso pom.xml:

  • A propriedade junit.test.tag especifica a tag que identifica os métodos de teste invocados. Como apenas os testes unitários são executados por padrão, o valor padrão desta propriedade deve ser UnitTest.
  • A propriedade skip.failsafe.plugin especifica se o Maven deve pular o Maven Failsafe Plugin (true por padrão).
  • A propriedade skip.unit.tests especifica se o Maven deve pular os testes unitários (false por padrão).

E criamos os três perfis (dev, end-to-end-test e integration-test):

 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
<properties>
    <junit.test.tag>UnitTest</junit.test.tag>
    <skip.failsafe.plugin>true</skip.failsafe.plugin>
    <skip.unit.tests>false</skip.unit.tests>
</properties>

<profiles>
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
    </profile>
    <profile>
        <id>end-to-end-test</id>
        <properties>
            <junit.test.tag>EndToEndTest</junit.test.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
    <profile>
        <id>integration-test</id>
        <properties>
            <junit.test.tag>IntegrationTest</junit.test.tag>
            <skip.failsafe.plugin>false</skip.failsafe.plugin>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>

Terceiro, configuramos o maven-surefire-plugin e o maven-failsafe-plugin usando <groups>${junit.test.tag}</groups>:

 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
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.2</version>
    <configuration>
        <skip>${skip.unit.tests}</skip>
        <groups>${junit.test.tag}</groups>
    </configuration>
</plugin>

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.5.2</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Test.kt</include>
        </includes>
        <skip>${skip.failsafe.plugin}</skip>
        <groups>${junit.test.tag}</groups>
    </configuration>
</plugin>

Agora configuramos nosso projeto. Vamos avançar e ver como escrever testes unitários, de integração e End-to-End simples usando @Tag no JUnit 6.

Escrevendo Nossos Testes Unitários, de Integração e End-to-End

O JUnit Jupiter (JUnit 5/6) possui um conceito chamado tag (@Tag) que nos permite dividir nossas classes e métodos de teste em diferentes categorias que podem ser usadas para filtrar quais testes são invocados.

Como temos que garantir que toda classe de teste possua pelo menos uma tag sem erros de digitação, é uma excelente prática criar um object TestTags que centraliza os valores constantes das nossas tags:

1
2
3
4
5
6
7
package com.testwithspring.master

object TestTags {
    const val END_TO_END_TEST = "EndToEndTest"
    const val INTEGRATION_TEST = "IntegrationTest"
    const val UNIT_TEST = "UnitTest"
}

Nosso próximo passo é escrever nossas classes de teste e configurar a categoria de cada classe anotando-a com @Tag:

Primeiro, criamos uma classe em src/test/kotlin chamada ExampleUnitTest e garantimos que seus métodos de teste sejam executados quando rodarmos nossos testes unitários:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.master

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag(TestTags.UNIT_TEST)
class ExampleUnitTest {

    @Test
    @DisplayName("Este método de teste deve ser executado quando rodarmos nossos testes unitários")
    fun shouldBeRunWhenUnitTestsAreRun() {
    }
}

Segundo, criamos uma classe em src/integration-test/kotlin chamada ExampleIntegrationTest e garantimos que seus métodos de teste sejam executados quando rodarmos nossos testes de integração:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.master

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag(TestTags.INTEGRATION_TEST)
class ExampleIntegrationTest {

    @Test
    @DisplayName("Este método de teste deve ser executado quando rodarmos nossos testes de integração")
    fun shouldBeRunWhenIntegrationTestsAreRun() {
    }
}

Terceiro, criamos uma classe em src/end-to-end-test/kotlin chamada ExampleEndToEndTest e garantimos que seus métodos de teste sejam executados quando rodarmos nossos testes End-to-End:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.master

import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag(TestTags.END_TO_END_TEST)
class ExampleEndToEndTest {

    @Test
    @DisplayName("Este método de teste deve ser executado quando rodarmos nossos testes End-to-End")
    fun shouldBeRunWhenEndToEndTestsAreRun() {
    }
}

Dica Idiomática Kotlin/JUnit 6:

Como o @Tag do JUnit Jupiter suporta meta-anotações, você também pode criar as anotações compostas @UnitTest, @IntegrationTest e @EndToEndTest anotadas com @Tag(TestTags.UNIT_TEST), etc., tornando suas classes de teste ainda mais expressivas!

Executando Nossos Testes Unitários, de Integração e End-to-End

Podemos executar apenas nossos testes unitários:

1
2
3
4
## Com Gradle 9:
./gradlew clean test
## Com Maven:
mvn clean test -P dev

Saída esperada:

1
2
> Task :test
com.testwithspring.master.ExampleUnitTest > Este método de teste deve ser executado quando rodarmos nossos testes unitários PASSED

Podemos executar apenas nossos testes de integração:

1
2
3
4
## Com Gradle 9:
./gradlew clean integrationTest
## Com Maven:
mvn clean verify -P integration-test

Saída esperada:

1
2
> Task :integrationTest
com.testwithspring.master.ExampleIntegrationTest > Este método de teste deve ser executado quando rodarmos nossos testes de integração PASSED

E podemos executar apenas nossos testes End-to-End:

1
2
3
4
## Com Gradle 9:
./gradlew clean endToEndTest
## Com Maven:
mvn clean verify -P end-to-end-test

Saída esperada:

1
2
> Task :endToEndTest
com.testwithspring.master.ExampleEndToEndTest > Este método de teste deve ser executado quando rodarmos nossos testes End-to-End PASSED

Vamos resumir o que aprendemos neste webinar.

Resumo

Este webinar nos ensinou três coisas:

  • Se quisermos configurar a tag que identifica os métodos de teste executados pelo Gradle (useJUnitPlatform) ou pelo Maven Surefire/Failsafe, podemos definir a propriedade includeTags / groups.
  • É uma excelente prática criar um object TestTags (ou meta-anotações compostas) que centraliza os valores das nossas tags sem risco de erros de digitação.
  • Podemos configurar a categoria de uma classe de teste ou de um método de teste utilizando a anotação @Tag do JUnit 6.