Executando Testes Unitários: Comparativo Maven vs Gradle

Ao concluirmos esta aula, nós:

  • Saberemos como executar nossos testes unitários com o Maven (usando o Maven Surefire Plugin com suporte nativo ao JUnit Platform 6).
  • Compreenderemos por que devemos categorizar nossos testes utilizando Tags do JUnit 6 (@Tag) (que substituíram as antigas @Category do JUnit 4).
  • Seremos capazes de executar exclusivamente os testes unitários filtrados pela sua categoria/tag no Maven.

Executando Testes Unitários com o Maven Surefire Plugin

Podemos executar nossos testes unitários em projetos Maven utilizando o Maven Surefire Plugin. A primeira coisa que devemos fazer é configurar o Maven Surefire Plugin na seção build do nosso arquivo pom.xml.

Para utilizar a configuração padrão do plugin em um projeto moderno com JUnit 6 e Kotlin, adicionamos o seguinte trecho à seção build/plugins do nosso arquivo pom.xml:

1
2
3
4
5
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.2</version>
</plugin>

[!NOTE] A partir da versão 2.22.0+ (e especialmente na série 3.x), o maven-surefire-plugin detecta e executa testes no JUnit Platform (JUnit Jupiter 6) automaticamente sem precisar de provedores extras!

Quando utilizamos a configuração padrão, o Maven Surefire Plugin executa todos os métodos de teste encontrados nas classes de teste que cumprem estas duas condições:

  • As classes de teste devem estar localizadas no diretório de código-fonte de testes (src/test/kotlin ou src/test/java).
  • O nome da classe de teste deve começar ou terminar com a palavra Test (ou terminar com Tests / TestCase).

[!TIP] Se os nossos testes unitários precisarem de arquivos de recursos, como arquivos .properties, .yml, .sql ou configurações de log (logback-test.xml), devemos colocar esses arquivos dentro do diretório src/test/resources.

Vamos escrever um teste unitário simples que imprime a string "The default configuration" no System.out (println). Após escrevermos esse teste em Kotlin com JUnit 6, o código-fonte da nossa classe DefaultTest fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.testwithspring.starter.unittests

import org.junit.jupiter.api.Test

class DefaultTest {

    @Test
    fun defaultTest() {
        println("The default configuration")
    }
}

Agora podemos executar nossos testes unitários rodando o seguinte comando no terminal:

1
mvn clean test

[!NOTE] Se não quisermos limpar (clean) todo o diretório target do projeto antes de rodar nossos testes unitários, basta executar o comando:

1
mvn test

Quando executamos nossos testes unitários, notamos no console que o Maven descobre e executa nossa classe DefaultTest:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.testwithspring.starter.unittests.DefaultTest
The default configuration
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.012 s -- in com.testwithspring.starter.unittests.DefaultTest

Results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Se o nosso projeto possuir apenas testes unitários, não precisamos fazer mais nenhuma alteração no nosso arquivo pom.xml. Contudo, em projetos reais, quase sempre escrevemos também testes de integração e testes End-to-End. Isso significa que precisamos encontrar uma maneira de executar nossos testes unitários rápidos sem disparar os testes de integração e End-to-End junto com eles!


Configurando a Categoria (@Tag) dos Nossos Testes Unitários

Por padrão, o Maven não diferencia testes unitários, testes de integração e testes End-to-End: ele simplesmente roda todos os testes encontrados nas classes cujos nomes começam ou terminam com Test. Podemos resolver esse problema usando uma destas duas opções:

  1. Opção 1: Usar convenções de nomenclatura diferentes no nome das classes de teste para diferenciar testes unitários (*Test), testes de integração (*IT ou *IntegrationTest) e testes End-to-End (*E2ETest). A desvantagem dessa opção é que somos forçados a adicionar um sufixo ou prefixo artificial ao nome de cada classe de teste apenas para agradar a ferramenta de build.
  2. Opção 2: Usar Categorias / Tags do JUnit (@Category no JUnit 4 ou @Tag no JUnit Jupiter 6). A única exigência dessa opção é anotar cada classe de teste com @Tag("unitTest") (ou uma meta-anotação customizada @UnitTest).

A opção número dois é uma escolha muito superior porque:

  • Ela não nos força a poluir o nome das nossas classes de teste com prefixos ou sufixos artificiais.
  • Ela nos dá a flexibilidade de criar subcategorias específicas e filtrar testes tanto em nível de classe quanto em nível de método individual.

Por exemplo, podemos criar duas tags diferentes para nossos testes de integração: uma para testes de integração rápidos que rodam na máquina local do desenvolvedor, e outra para testes de integração pesados que só rodam no servidor de Integração Contínua (CI).

[!NOTE] Evolução do JUnit 4 (@Category) para o JUnit 6 (@Tag) Como vimos na aula Arquitetura do JUnit Platform e Test Engines, no JUnit 4 era necessário criar uma interface marcadora vazia (interface UnitTest) e anotar a classe com @Category(UnitTest::class).

No JUnit Jupiter 6, o mecanismo @Category foi substituído pela anotação nativa @Tag("unitTest") — que aceita uma simples String e pode ser combinada com meta-anotações em Kotlin!

Vamos demonstrar isso na prática criando uma nova classe de teste chamada CategoryTest (ou TaggedUnitTest). Essa classe contém um teste unitário que imprime a string "The category: UnitTest" no console.

Para manter a mesma segurança de tipos que tínhamos com a interface marcadora do JUnit 4 sem risco de erros de digitação na string da tag, podemos declarar uma constante ou uma meta-anotação @UnitTest:

1
2
3
4
5
6
7
8
9
package com.testwithspring.starter.unittests

import org.junit.jupiter.api.Tag

// Meta-anotação opcional no JUnit 6 (substitui @Category(UnitTest::class) do JUnit 4)
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@Tag("unitTest")
annotation class UnitTest

E aplicamos @Tag("unitTest") (ou @UnitTest) à nossa classe CategoryTest:

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

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

@Tag("unitTest") // Equivalente moderno ao @Category(UnitTest.class) do JUnit 4
class CategoryTest {

    @Test
    fun categoryTest() {
        println("The category: UnitTest")
    }
}

Depois de criarmos nossa classe de teste e configurarmos sua categoria/tag, precisamos garantir que o Maven Surefire Plugin execute apenas os testes que pertencem à tag unitTest (no JUnit 4 colocávamos o nome completo da interface com.testwithspring.starter.unittests.UnitTest dentro da tag <groups>; no JUnit 6 colocamos o nome da tag unitTest na mesma tag <groups>!).

Após configurarmos a categoria/tag no pom.xml, a configuração do Maven Surefire Plugin fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.5.2</version>
    <configuration>
        <!-- No JUnit 6 passamos o nome da @Tag("unitTest") -->
        <!-- (No JUnit 4 era o FQN da interface: com.testwithspring.starter.unittests.UnitTest) -->
        <groups>unitTest</groups>
    </configuration>
</plugin>

Quando executamos mvn clean test agora, notamos que o Maven executa apenas o teste encontrado na classe CategoryTest (que possui @Tag("unitTest")) e ignora completamente o teste da classe DefaultTest (que não possui a tag):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.testwithspring.starter.unittests.CategoryTest
The category: UnitTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.011 s -- in com.testwithspring.starter.unittests.CategoryTest

Results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Vamos resumir o que aprendemos nesta aula!


Resumo

Nesta aula, aprendemos três pontos fundamentais:

  • Podemos executar nossos testes unitários em projetos Maven utilizando o Maven Surefire Plugin.
  • Por padrão, o Maven não diferencia testes unitários, testes de integração e testes End-to-End.
  • Devemos categorizar nossos testes utilizando @Tag no JUnit 6 (o substituto moderno de @Category do JUnit 4) e configurar o elemento <groups> do Maven Surefire Plugin para executar apenas a categoria desejada.