Sumário
1. Introdução
2. Tecnologias e Ferramentas
3. Instalando o JDK e o Gradle
3.1. Instalando o SDKMAN!
3.2. Instalando o JDK
3.3. Instalando o Gradle
4. Criando um projeto Kotlin com o Gradle
4.1. Estruturando o projeto com o Gradle
4.2. Configurando o projeto no IntelliJ
4.3. Configurando o Kotlin no projeto
5. Desenvolvendo testes com o JUnit
5.1. Configurando o JUnit 5
5.2. Escrevendo Casos de Teste
5.3. Parametrizando testes
5.4. Setup e Teardown
5.5. Assertions úteis com Kotlin
6. Resumo
7. Conclusão
8. Referências
1. Introdução
A automatização de testes funcionais é fundamental para garantir a qualidade de um software à medida em que novas funcionalidades são desenvolvidas e entregues em produção. Quanto maior e mais complexo o software se torna, menos viável fica testar manualmente todas as features e garantir a ausência de regressões. Uma suíte de testes automatizados, com cobertura das principais funcionalidades, possibilita executarmos os testes a cada modificação feita no software, obtendo rapidamente um feedback do estado da qualidade do sistema.
Neste artigo, vamos desenvolver testes simples utilizando Kotlin e JUnit 5, estruturando e configurando um projeto do zero com o Gradle. Veremos algumas das principais funcionalidades do JUnit, simulando diferentes cenários de automação de testes. Além disso, vamos destacar as maiores diferenças entre o uso do JUnit com o Kotlin, comparado ao uso com o Java.
O público-alvo deste artigo são tanto QAs quanto desenvolvedores iniciantes na automação de testes funcionais com Kotlin, JUnit e Gradle, ou aqueles que já são familiarizados com JUnit e Java mas estão iniciando no Kotlin. Caso você esteja buscando apenas um passo-a-passo para a estruturação de um novo projeto, é possível pular diretamente para a seção 6. Resumo deste artigo. E caso você esteja buscando somente um projeto para usar como referência, é possível consultar o repositório no GitHub, que contém todo o código que será apresentado aqui.
Vamos desenvolver do zero um projeto de automação de testes funcionais utilizando Kotlin, JUnit 5 e Gradle. Cobriremos desde a configuração do ambiente de desenvolvimento, passando pela instalação e configuração de cada uma das ferramentas, até a criação da estrutura do projeto com o Gradle, e desenvolvimento dos testes utilizando o IntelliJ. Veremos alguns dos principais conceitos e funcionalidades do JUnit 5 e como eles podem ser utilizados em conjunto com o Kotlin. Por fim, faremos um breve comparativo entre o Kotlin e o Java, com relação ao uso de algumas validações oferecidas pelo JUnit.
Uma boa cobertura de testes funcionais automatizados é essencial para reduzir o número de bugs encontrados em produção. Além de verificar o estado do sistema, os testes automatizados também servem como uma documentação das funcionalidades, descrevendo o comportamento esperado de um software para cada cenário. Veja neste artigo como o JUnit, combinado à sintaxe moderna do Kotlin, pode ajudar a automatizar diferentes necessidades de casos de teste, resultando em testes legíveis e concisos.
2. Tecnologias e Ferramentas
Para desenvolver e executar todos os testes desenvolvidos neste artigo, utilizaremos as seguintes ferramentas:
- Kotlin
- JUnit 5
- Gradle
- JDK
- SDKMAN! (opcional)
- IntelliJ IDEA
- Terminal Unix
O Kotlin é uma linguagem de desenvolvimento moderna, concisa, multiplataforma, capaz de rodar na JVM, e interoperável com Java. Com ele vamos desenvolver casos de testes automatizados, em conjunto com o JUnit 5.[1]
Como framework para desenvolvimento e execução de testes na JVM, vamos usar o JUnit 5. Este framework possui funcionalidades que atendem a diversas necessidades e estratégias de teste, além de ter suporte nas principais ferramentas de build de projetos e nos principais IDEs.[2]
Para a automação dos processos de build e de gerenciamento de dependências do projeto, usaremos o Gradle. Com ele vamos estruturar e configurar o projeto para a utilização do Kotlin como linguagem de desenvolvimento e do JUnit como framework de testes. [3]
O Java Development Kit (JDK) é requisito para utilização do Gradle, e fornece suporte ao desenvolvimento com o Kotlin para a JVM.[4][5]
Utilizaremos o SDKMAN! apenas pela praticidade de instalação e gerenciamento de versões do JDK e do Gradle. Caso desejado, é possível utilizar outras formas de instalação destas ferramentas no seu sistema operacional.[6]
Como ambiente de desenvolvimento vamos usar o IntelliJ IDEA Community Edition, que pode ser baixado a partir do site da JetBrains, selecionando a versão específica para o seu sistema operacional. Por padrão, o IntelliJ já vem com suporte ao Kotlin incluso.[1]
E para a instalação das ferramentas e execução dos testes, vamos utilizar um terminal compatível com sistemas UNIX, como Linux ou macOS. É possível utilizar qualquer terminal compatível de sua preferência, que utilize os shells Bash ou Zsh.
A instalação e configuração de cada uma destas ferramentas serão abordadas nas seções seguintes.
3. Instalando o JDK e o Gradle
Para o desenvolvimento do projeto deste artigo, iremos utilizar o IntelliJ IDEA. Através deste IDE, é possível criar toda a estrutura de um novo projeto, delegando para ele, inclusive, a instalação e o gerenciamento das versões do JDK e do Gradle.
Porém, para criar a estrutura inicial do nosso projeto, vamos seguir uma abordagem diferente, utilizando o inicializador de projeto do Gradle pela command-line. Desta forma, iremos adicionar, gradativamente, apenas as configurações necessárias ao nosso projeto, além de utilizarmos uma estrutura de projeto atualizada com as funcionalidades e recomendações mais recentes do Gradle.
Para instalar o JDK e o Gradle, vamos utilizar o SDKMAN!, que é um gerenciador de versões de SDKs. Mas o SDKMAN! não é um requisito para o nosso projeto. Caso prefira seguir com alguma outra forma de instalação, tanto para o JDK quanto para o Gradle, é perfeitamente possível. Desde que o processo resulte em um ambiente onde estas duas ferramentas estejam disponíveis.
3.1. Instalando o SDKMAN!
Vamos começar instalando o SDKMAN! no nosso ambiente. Esta ferramenta tem como requisito para funcionamento um ambiente bash, sendo compatível com shells Bash e ZSH. Sistemas UNIX como Linux, macOS e Windows com WSL possuem shells compatíveis. Caso deseje utilizar o Windows sem o WSL, consulte na documentação do SDKMAN! as formas alternativas de instalação.
Abra o terminal de sua preferência, e rode o comando para instalação do SDKMAN!:
curl -s "https://get.sdkman.io" | bash
Siga as instruções exibidas. Em seguida, reinicie o terminal, ou rode o seguinte comando:
source "$HOME/.sdkman/bin/sdkman-init.sh"
Por fim, verifique se a instalação ocorreu com sucesso:
sdk version
Espera-se que sejam exibidas as versões da ferramenta, como no exemplo abaixo:
SDKMAN!
script: 5.18.2
native: 0.4.6
3.2. Instalando o JDK
Agora que temos o SDKMAN! configurado, vamos utilizá-lo para instalar o JDK. Neste artigo, vamos usar o JDK 21, que atualmente é a última versão LTS, e vamos usar a distribuição Amazon Corretto:
sdk install java 21.0.6-amzn
Após a execução, veremos algo parecido com a saída a seguir:
Downloading: java 21.0.6-amzn
In progress...
########################################################## 100.0%
Repackaging Java 21.0.6-amzn...
Done repackaging...
Cleaning up residual files...
Installing: java 21.0.6-amzn
Done installing!
Do you want java 21.0.6-amzn to be set as default? (Y/n): y
Setting java 21.0.6-amzn as default.
Configurando a versão como default, todos os terminais abertos após isso utilizarão esta versão por padrão.
Após isso, podemos verificar a versão do Java que acabamos de configurar:
java -version
E verificamos a seguinte saída:
openjdk version "21.0.6" 2025-01-21 LTS
OpenJDK Runtime Environment Corretto-21.0.6.7.1 (build 21.0.6+7-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.6.7.1 (build 21.0.6+7-LTS, mixed mode, sharing)
3.3. Instalando o Gradle
Com o Java instalado, podemos instalar o Gradle:
sdk install gradle 8.13
Em seguida podemos verificar a instalação:
gradle -v
E temos a seguinte saída:
------------------------------------------------------------
Gradle 8.13
------------------------------------------------------------
Build time: 2025-02-25 09:22:14 UTC
Revision: 073314332697ba45c16c0a0ce1891fa6794179ff
Kotlin: 2.0.21
Groovy: 3.0.22
Ant: Apache Ant(TM) version 1.10.15 compiled on August 25 2024
Launcher JVM: 21.0.6 (Amazon.com Inc. 21.0.6+7-LTS)
Daemon JVM: /Users/daniel.mertins/.sdkman/candidates/java/21.0.6-amzn (no JDK specified, using current Java home)
OS: Mac OS X 15.3.1 x86_64
4. Criando um projeto Kotlin com o Gradle
Uma vez instaladas todas as dependências e ferramentas, podemos criar a estrutura inicial do nosso projeto de automação de testes. Para isso, vamos utilizar o Build Init Plugin do Gradle. Embora também seja possível criar um projeto pelo Wizard do IntelliJ, ao utilizarmos o Gradle Init, o resultado será uma estrutura mais atualizada com as funcionalidades e recomendações mais recentes do Gradle.
4.1. Estruturando o projeto com o Gradle
Vamos iniciar criando um diretório para o projeto:
mkdir kotlin-junit5-test-automation
cd kotlin-junit5-test-automation
Em seguida, dentro do novo diretório, vamos usar a task init do Gradle para criar uma estrutura básica de projeto:
gradle init \
--type basic \
--dsl kotlin \
--no-comments \
--use-defaults
Será criada a seguinte estrutura, contendo a configuração básica de um projeto Gradle:
.
├── gradle
│ ├── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ └── libs.versions.toml
├── .gitattributes
├── .gitignore
├── build.gradle.kts
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
4.2. Configurando o projeto no IntelliJ
Agora que temos a estrutura do projeto criada, vamos abrir ele no IntelliJ. Por padrão, o IntelliJ vem com os plugins do Kotlin e do Gradle instalados, oferecendo o suporte necessário para trabalharmos com estas duas tecnologias.
Abra o IDE, acesse Projects | Open, e selecione o diretório criado para o nosso projeto. O IntelliJ fará o carregamento do projeto, resultando em uma visualização próxima ao print a seguir:
Primeiramente vamos verificar se as configurações do IDE para o JDK e para o Gradle estão corretas. Desta forma, garantimos que as funcionalidades de assistência de código e de integração com o Gradle funcionem de acordo com as versões destas ferramentas que instalamos em nosso ambiente.
Para configurar a versão do JDK do projeto, no menu do IntelliJ vá em File | Project Structure | Project Settings | Project, e certifique-se que o SDK do projeto está configurado com a versão correta do JDK, e que o Language Level esteja configurado como SDK default
:[7]
Para acessar as configurações do Gradle, vá no menu IntelliJ IDEA | Settings | Build , Execution, Deployment | Build Tools | Gradle. Certifique-se que as opções de Build and Run estejam configuradas com Gradle
, que o Gradle Distribution seja Wrapper
, e que o Gradle JVM seja Project SDK
:
4.3. Configurando o Kotlin no projeto
O Gradle oferece suporte a diferentes linguagens através de um sistema de plugins. Além dos Core plugins que o Gradle desenvolve e mantém, podemos encontrar diversos plugins compartilhados pela comunidade no Gradle Plugin Portal. E para configurar um projeto Kotlin para rodar na JVM com o Gradle, utilizaremos o plugin Kotlin JVM, desenvolvido pela JetBrains.[8]
A primeira coisa que faremos será a configuração da versão do Kotlin que o projeto irá utilizar. Para isso vamos usar o arquivo libs.version.toml
, que é o Catálogo de Versões para o gerenciamento de dependências do Gradle. Neste arquivo podemos centralizar a configuração de todas as versões das dependências diretas do projeto.[9]
Abra o arquivo gradle/libs.versions.toml
, e defina a versão do Kotlin:
[versions]
kotlin = "2.1.10"
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Após definirmos o Kotlin JVM no catálogo, podemos incluí-lo no arquivo de build do projeto, que é o arquivo build.gradle.kts
. Porém, antes disso, vamos criar um subprojeto Gradle para a nossa aplicação de testes.
Por padrão, a convenção de estrutura do código fonte de um projeto Kotlin apresenta os seguintes diretórios:[10]
.
└── src
├── main
│ └── kotlin
└── test
└── kotlin
Na raiz do projeto, crie a estrutura de diretórios:
mkdir -p app/src/main/kotlin app/src/test/kotlin
E mova o arquivo build.gradle.kts
para o subprojeto app
:
mv build.gradle.kts app
Ao final, a estrutura do projeto deve ser a seguinte:
.
├── app
│ ├── src
│ │ ├── main
│ │ │ └── kotlin
│ │ └── test
│ │ └── kotlin
│ └── build.gradle.kts
├── gradle
│ ├── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
│ └── libs.versions.toml
├── .gitattributes
├── .gitignore
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
Por fim, no arquivo settings.gradle.kts
vamos definir no projeto Gradle o subprojeto app
que acabamos de criar:
rootProject.name = "kotlin-junit5-test-automation"
include("app")
Agora que criamos o subprojeto app
, vamos configurá-lo para utilizar o plugin Kotlin JVM que definimos no catálogo. Abra o arquivo app/build.gradle.kts
e aplique o plugin com o bloco plugins{}
da DSL de plugins do Gradle, e declare o repositório Maven Central, que será utilizado para buscar as dependências adicionadas ao projeto:[11]
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories {
mavenCentral()
}
Com isso, estamos prontos para iniciar o desenvolvimento com Kotlin no módulo app
. Vamos começar incluindo um objeto que simula uma calculadora simples, contendo as quatro operações básicas. Usaremos esse objeto inicialmente para validar a configuração do módulo, e posteriormente para o desenvolvimento de alguns testes automatizados.[12]
Crie o arquivo app/src/main/kotlin/Calculator.kt
, e inclua o objeto Calculator
e a função main
:
object Calculator {
fun add(a: Int, b: Int) = a + b
fun subtract(a: Int, b: Int) = a - b
fun multiply(a: Int, b: Int) = a * b
fun divide(a: Int, b: Int) = a / b
}
fun main() {
val a = 8
val b = 2
println("$a + $b = ${Calculator.add(a, b)}")
println("$a - $b = ${Calculator.subtract(a, b)}")
println("$a * $b = ${Calculator.multiply(a, b)}")
println("$a / $b = ${Calculator.divide(a, b)}")
}
E podemos executar a função main
através do ícone de execução exibido pelo IntelliJ, logo à esquerda da declaração da função. A saída gerada pelo programa pode ser visualizada na janela de execução:
Temos agora o projeto Kotlin configurado corretamente, e com um programa simples que vamos utilizar no desenvolvimento de testes automatizados com o JUnit na próxima seção.
5. Desenvolvendo testes com o JUnit
O JUnit 5, diferentemente das versões anteriores, é composto por vários módulos de três diferentes subprojetos:[2]
JUnit Platform: responsável pela descoberta e execução dos testes na JVM.
JUnit Jupiter: responsável pelas APIs para desenvolvimento de testes e extensões no JUnit 5.
JUnit Vintage: oferece suporte para a execução de testes desenvolvidos com versões anteriores do JUnit.
Neste artigo utilizaremos o JUnit Platform e o JUnit Jupiter.
5.1. Configurando o JUnit 5
Para configurar o JUnit no nosso projeto, vamos começar incluindo as dependências do agregador JUnit Jupiter e do JUnit Platform Launcher no catálogo de versões. O junit-jupiter
irá incluir todos os módulos do JUnit Jupiter nas dependências do projeto, proporcionando um gerenciamento de dependências simplificado. Enquanto o junit-platform-launcher
será utilizado para configurar a detecção e execução dos testes com o Gradle.[13][14]
A versão atual do junit-jupiter
é a 5.12.0
, e a do junit-platform-launcher
é a 1.12.0
. Porém, em vez de especificar individualmente as versões destes dois módulos, faremos uso de um terceiro módulo do JUnit, responsável por fazer o alinhamento das versões de diferentes artefatos do JUnit – o JUnit Bill of Materials (BOM). Desta forma, podemos especificar apenas a versão do junit-bom
, e omitir a versão dos outros artefatos do JUnit que utilizarmos no projeto.[15]
No catálogo, inclua o junit-bom
, o junit-jupiter
e o junit-platform-launcher
:
[versions]
junit-bom = "5.12.0"
kotlin = "2.1.10"
[libraries]
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit-bom" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Em seguida, vamos configurar as dependências no arquivo build.gradle.kts
. O junit-bom
deve ser importado como uma dependência testImplementation
, utilizando o método modificador platform
.[15][16] O junit-jupiter
deve ser definido como dependência testImplementation
, enquanto o junit-platform-launcher
deve ser definido como uma dependência testRuntimeOnly
. E além das dependências, precisamos habilitar o suporte ao JUnit Platform para descoberta e execução dos testes no Gradle.[17][18]
Por fim, também vamos configurar os resultados que queremos ver no terminal ao executar nossa suíte de testes. Por padrão, a tarefa de teste do Gradle exibe na saída do terminal apenas os eventos do tipo FAILED
.[19] Vamos incluir uma configuração para que sejam exibidos também os eventos PASSED
e SKIPPED
, para podermos visualizar também os testes que passaram e os que não foram executados. Essa configuração pode ser feita na propriedade testLogging
da tarefa de teste do Gradle. Além disso, vamos configurar o formato de exibição de exceções, para que em caso de falhas, as mensagens completas das validações sejam exibidas.[20][21]
Abaixo temos o resultado esperado do arquivo build.gradle.kts
após as configurações:
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.jupiter)
testRuntimeOnly(libs.junit.platform.launcher)
}
tasks.test {
useJUnitPlatform()
testLogging {
events(TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED)
exceptionFormat = TestExceptionFormat.FULL
}
}
Com estas configurações, temos o projeto pronto para começar a desenvolver e executar casos de teste com o JUnit.
5.2. Escrevendo Casos de Teste
O JUnit Jupiter oferece um modelo de programação para a configuração de testes com a utilização de annotations. O requisito mínimo para a escrita de um teste no JUnit Jupiter é a declaração de um método de teste com a annotation @Test
. Vamos criar o arquivo app/src/test/kotlin/CalculatorTest.kt
e começar escrevendo alguns casos de teste para a nossa calculadora usando a annotation @Test
e o método de validação assertEquals()
:[22]
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class CalculatorTest {
@Test fun addition() {
assertEquals(2, Calculator.add(1, 1))
}
@Test fun subtraction() {
assertEquals(1, Calculator.subtract(2, 1))
}
@Test fun multiplication() {
assertEquals(4, Calculator.multiply(2, 2))
}
@Test fun division() {
assertEquals(3, Calculator.divide(6, 2))
}
}
E agora, no terminal, vamos executar os testes com a tarefa de teste do Gradle:
./gradlew test
Veremos a seguinte saída:
Calculating task graph as no cached configuration is available for tasks: test
> Task :app:test
CalculatorTest > subtraction() PASSED
CalculatorTest > addition() PASSED
CalculatorTest > division() PASSED
CalculatorTest > multiplication() PASSED
BUILD SUCCESSFUL in 17s
4 actionable tasks: 4 executed
Configuration cache entry stored.
Para vermos algum dos testes não passar, podemos alterar o valor esperado no método assertEquals()
, forçando uma falha do caso de teste. Por exemplo, alterando o resultado esperado do teste de divisão de 3
para 4
, ao executar os testes novamente temos a seguinte saída:
Reusing configuration cache.
> Task :app:test FAILED
CalculatorTest > subtraction() PASSED
CalculatorTest > addition() PASSED
CalculatorTest > division() FAILED
org.opentest4j.AssertionFailedError: expected: <4> but was: <3>
at app//org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at app//org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at app//org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
at app//org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:145)
at app//org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:531)
at app//CalculatorTest.division(CalculatorTest.kt:19)
CalculatorTest > multiplication() PASSED
4 tests completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:test'.
> There were failing tests. See the report at: file:///Users/daniel.mertins/Programming/kotlin-junit5-test-automation/app/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 4s
4 actionable tasks: 2 executed, 2 up-to-date
Configuration cache entry reused.
Agora que sabemos como definir um caso de teste e fazer validações de valores, nas seções seguintes vamos explorar outras annotations e métodos de validação do JUnit.
5.3. Parametrizando testes
Na seção anterior nós escrevemos um único método de teste para cada operação da calculadora. Porém, estamos testando cada operação com um único conjunto de entradas, e para aumentar a nossa cobertura de teste, queremos validar o resultado das operações para diferentes valores de entrada.
Uma forma de aumentar a cobertura de teste seria escrever um método de teste para cada conjunto de entradas e saída esperados para cada operação. Mas essa abordagem resultaria em uma grande repetição de código.
Para situações como esta, podemos utilizar a funcionalidade de parametrização de testes do JUnit. Testes parametrizados podem ser executados múltiplas vezes com diferentes argumentos. Para declarar este tipo de método, utilizamos a annotation @ParameterizedTest
em vez de @Test
. Além disso, precisamos especificar no mínimo uma fonte, que irá fornecer os argumentos para cada execução, e consumir estes argumentos dentro do método de teste.[23]
O exemplo a seguir, criado no arquivo CalculatorParameterizedTest.kt
, mostra um teste parametrizado para a operação de adição, usando a anotação @CsvSource
, que permite definirmos listas de argumentos expressados como strings CSV:[24]
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class CalculatorParameterizedTest {
@ParameterizedTest
@CsvSource(
" 1, 1, 2",
" 2, -1, 1",
"-2, 1, -1",
"-2, -2, -4",
" 0, 1, 1",
"-1, 0, -1",
" 0, 0, 0",
)
fun addition(a: Int, b: Int, result: Int) {
assertEquals(result, Calculator.add(a, b))
}
}
Para executar apenas esta classe de teste, podemos usar a opção --tests
do Gradle:[25]
./gradlew test --tests=CalculatorParameterizedTest
Na saída, vemos que é gerado um teste para cada string de argumentos da lista:
Starting a Gradle Daemon (subsequent builds will be faster)
Reusing configuration cache.
> Task :app:test
CalculatorParameterizedTest > addition(int, int, int) > [1] 1, 1, 2 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [2] 2, -1, 1 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [3] -2, 1, -1 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [4] -2, -2, -4 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [5] 0, 1, 1 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [6] -1, 0, -1 PASSED
CalculatorParameterizedTest > addition(int, int, int) > [7] 0, 0, 0 PASSED
BUILD SUCCESSFUL in 21s
4 actionable tasks: 2 executed, 2 up-to-date
Configuration cache entry reused.
Caso necessário, podemos deixar ainda mais descritivas, tanto a fonte de dados, quanto a saída da execução dos testes, especificando uma linha de cabeçalho contendo o nome de cada um dos argumentos:
class MultiplicationParameterizedTest {
@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, value = [
" a, b, result",
" 1, 1, 1",
"-1, 2, -2",
" 2, -3, -6",
"-2, -2, 4",
])
fun `multiplication sign`(a: Int, b: Int, result: Int) {
assertEquals(result, Calculator.multiply(a, b))
}
@ParameterizedTest
@CsvSource(useHeadersInDisplayName = true, value = [
" a, b, result",
" 0, 1, 0",
" 0, -2, 0",
" 2, 0, 0",
"-3, 0, 0",
" 0, 0, 0",
])
fun `multiply by zero`(a: Int, b: Int, result: Int) {
assertEquals(result, Calculator.multiply(a, b))
}
}
Vemos que agora a saída no terminal contém também o nome de cada um dos argumentos para os métodos de teste gerados:
Calculating task graph as no cached configuration is available for tasks: test --tests=MultiplicationParameterizedTest
> Task :app:test
MultiplicationParameterizedTest > multiplication sign(int, int, int) > [1] a = 1, b = 1, result = 1 PASSED
MultiplicationParameterizedTest > multiplication sign(int, int, int) > [2] a = -1, b = 2, result = -2 PASSED
MultiplicationParameterizedTest > multiplication sign(int, int, int) > [3] a = 2, b = -3, result = -6 PASSED
MultiplicationParameterizedTest > multiplication sign(int, int, int) > [4] a = -2, b = -2, result = 4 PASSED
MultiplicationParameterizedTest > multiply by zero(int, int, int) > [1] a = 0, b = 1, result = 0 PASSED
MultiplicationParameterizedTest > multiply by zero(int, int, int) > [2] a = 0, b = -2, result = 0 PASSED
MultiplicationParameterizedTest > multiply by zero(int, int, int) > [3] a = 2, b = 0, result = 0 PASSED
MultiplicationParameterizedTest > multiply by zero(int, int, int) > [4] a = -3, b = 0, result = 0 PASSED
MultiplicationParameterizedTest > multiply by zero(int, int, int) > [5] a = 0, b = 0, result = 0 PASSED
BUILD SUCCESSFUL in 24s
4 actionable tasks: 2 executed, 2 up-to-date
Configuration cache entry stored.
E os nomes dos testes parametrizados ainda podem ser customizados em um maior nível de detalhamento através do parâmetro name
da anotação @ParameterizedTest
. Não vamos explorar esta configuração neste artigo, mas é possível consultar todas as opções desta funcionalidade na documentação do JUnit.[26]
É importante destacar que algumas anotações do JUnit precisam de configuração adicional para que possam ser aplicadas em um método. O JUnit possui o conceito de Test Instance Lifecycle, que por padrão, está configurado no modo "per-method". Neste modo, o comportamento do JUnit Jupiter é criar sempre uma nova instância da classe de teste antes de executar cada método de teste.[27]
A anotação @MethodSource
por exemplo, que permite fazer referência a métodos factory como fonte de dados, requer que no modo "per-method" o método referenciado seja um método estático – um conceito do Java que não existe no Kotlin – mas que pode ser implementado na JVM com o uso de companion objects e a anotação @JvmStatic
.[28][29][30]
O exemplo a seguir implementa um método de teste parametrizado com um método factory estático como fonte de dados, em uma classe no modo "per-method" (default) do JUnit:
class SubtractionMethodSourceParameterizedTest {
@ParameterizedTest
@MethodSource("parametersProvider")
fun subtraction(a: Int, b: Int, result: Int) {
assertEquals(result, Calculator.subtract(a, b))
}
companion object {
@JvmStatic fun parametersProvider(): Array<Arguments> {
return arrayOf(
arguments(1, 1, 0),
arguments(1, -1, 2),
arguments(-1, 1, -2),
arguments(-1, -1, 0),
arguments(0, 0, 0),
)
}
}
}
Também é possível usar um método de instância como fonte de dados, configurando o lifecycle da classe para o modo "per-class". Esta abordagem resulta em um código relativamente mais conciso que a abordagem do método estático, que vimos no exemplo anterior. Porém esta opção só funciona se o método factory estiver dentro da mesma classe em que se encontra o método de teste parametrizado.[28] O exemplo a seguir mostra essa configuração:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DivisionMethodSourceParameterizedTest {
@ParameterizedTest
@MethodSource("parametersProvider")
fun division(a: Int, b: Int, result: Int) {
assertEquals(result, Calculator.divide(a, b))
}
fun parametersProvider(): Array<Arguments> {
return arrayOf(
arguments(2, 1, 2),
arguments(1, 1, 1),
arguments(-2, 1, -2),
arguments(-1, 1, -1),
arguments(0, 1, 0),
)
}
}
Nesta seção vimos apenas dois tipos de fontes de argumentos que o JUnit oferece para a parametrização de testes, aplicados em um caso típico de dados estruturados em strings CSV, e um caso de método factory. Porém existem outras fontes de argumentos disponíveis que atendem a diferentes necessidades, e que podem também ser consultadas na documentação do JUnit.[31]
5.4. Setup e Teardown
Existem cenários onde é necessário gerenciar recursos externos aos testes, como por exemplo os testes de interações com uma base de dados. Neste cenário, podemos adotar uma estratégia de criação de uma base de dados de teste no início da execução, e diferentes métodos de teste estabelecendo uma conexão e interagindo com esta base de dados. E ao final da execução, podemos fazer a exclusão desta base de teste.
O JUnit 5 oferece 4 anotações que atendem exatamente essa necessidade:[22]
Anotação | Descrição |
---|---|
@BeforeAll |
Define que o método será executado antes de todos os métodos de teste de uma classe. |
@BeforeEach |
Define que o método será executado antes de cada método de teste na classe atual. |
@AfterEach |
Define que o método será executado depois de cada método de teste na classe atual. |
@AfterAll |
Define que o método será executado depois de todos os métodos de teste na classe atual. |
Além disso, vamos usar a anotação @TestInstance
para configurar o ciclo de vida da classe de teste. Por padrão, o JUnit 5 cria uma nova instância de cada classe de teste antes de executar cada método de teste. Esta configuração é chamada de "per-method", e permite que métodos de teste individuais sejam executados em isolamento, e evita que ocorram efeitos colaterais indesejados em função de alterações no estado da instância de teste. Porém, na estratégia que vamos seguir, queremos que a base de dados de teste seja criada uma única vez, no início da execução dos testes, e que todos os testes tenham acesso a esta mesma base de dados. Ou seja, queremos que o estado da classe de teste seja compartilhado entre todos os métodos de teste. Para isso, usaremos a configuração "per-class", fazendo com que o JUnit crie apenas uma instância da classe de teste.[27]
No exemplo a seguir, não vamos implementar de fato uma conexão com uma base de dados. Porém vamos simular alguns cenários comuns quando testamos interações com bases de dados. No arquivo DatabaseTest.kt
, inclua a seguinte classe de teste:
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DatabaseTest {
@BeforeAll fun createDatabase() {
println("[@BeforeAll] Creating database...")
}
@BeforeEach fun connectToDatabase() {
println("[@BeforeEach] Connecting to database...")
}
@BeforeEach fun insertDataIntoDatabase() {
println("[@BeforeEach] Inserting data into database...")
}
@Test fun testDatabaseFunctionality() {
println("[@Test] Testing database functionality...")
}
@AfterEach fun deleteDataFromDatabase() {
println("[@AfterEach] Deleting data from database...")
}
@AfterEach fun disconnectFromDatabase() {
println("[@AfterEach] Disconnecting from database...")
}
@AfterAll fun destroyDatabase() {
println("[@AfterAll] Deleting database...")
}
}
Podemos executar a classe de teste pelo IntelliJ, e visualizar na saída do IDE a ordem de execução de cada método:
Também podemos visualizar esta mesma saída no Gradle Report, abrindo o arquivo app/build/reports/tests/test/index.html
, no IntelliJ ou navegador:
E se desejarmos ver a saída também no terminal, podemos incluir o evento STANDARD_OUT
na configuração do testLogging
no arquivo build.gradle.kts
:
tasks.test {
useJUnitPlatform()
testLogging {
events(
TestLogEvent.FAILED,
TestLogEvent.PASSED,
TestLogEvent.SKIPPED,
TestLogEvent.STANDARD_OUT
)
exceptionFormat = TestExceptionFormat.FULL
}
}
E podemos rodar os testes pelo terminal:
Reusing configuration cache.
> Task :app:test
DatabaseTest STANDARD_OUT
[@BeforeAll] Creating database...
DatabaseTest > testDatabaseFunctionality() STANDARD_OUT
[@BeforeEach] Connecting to database...
[@BeforeEach] Inserting data into database...
[@Test] Testing database functionality...
[@AfterEach] Deleting data from database...
[@AfterEach] Disconnecting from database...
DatabaseTest > testDatabaseFunctionality() PASSED
DatabaseTest STANDARD_OUT
[@AfterAll] Deleting database...
BUILD SUCCESSFUL in 2s
4 actionable tasks: 2 executed, 2 up-to-date
Configuration cache entry reused.
5.5. Assertions úteis com Kotlin
Nas seções anteriores desenvolvemos os testes usando apenas um único método de assertion do JUnit – assertEquals()
. Porém, diversos outros métodos de assertion estão disponíveis, que atendem a diferentes necessidades de validações, e que podem inclusive tornar nossos testes mais concisos e legíveis.[32]
Além disso, alguns assertions funcionam muito bem com a sintaxe do Kotlin. A seguir veremos algumas diferenças destes assertions, quando são usados com Java e quando são usados com Kotlin.
Mas antes de escrever os testes, vamos fazer algumas alterações na Calculadora desenvolvida na seção anterior, para ajustar a interoperabilidade com o Java. Podemos utilizar código escrito em Kotlin junto com código Java. E para fazer as chamadas dos métodos da Calculadora nos testes em Java, usando exatamente com e mesma sintaxe que usamos nos testes em Kotlin, vamos reescrever a Calculadora como uma classe, para que seja possível expor métodos estáticos para o Java:
class Calculator {
companion object {
@JvmStatic fun add(a: Int, b: Int) = a + b
@JvmStatic fun subtract(a: Int, b: Int) = a - b
@JvmStatic fun multiply(a: Int, b: Int) = a * b
@JvmStatic fun divide(a: Int, b: Int) = a / b
}
}
É importante destacar que esta alteração não impacta os testes em Kotlin que já escrevemos até aqui. Os métodos da calculadora continuam sendo chamados da mesma forma de antes.
Agora vamos explorar outros métodos de assertion, primeiramente observando a sintaxe com Java. No arquivo app/src/test/java/JavaAssertionsDemoTest.java
, vamos criar a seguinte classe de teste:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class JavaAssertionsDemoTest {
@Test void expectedExceptionTesting() {
var exception = assertThrows(ArithmeticException.class,
() -> Calculator.divide(1, 0)
);
assertEquals("/ by zero", exception.getMessage());
}
@Test void exceptionAbsenceTesting() {
assertDoesNotThrow(() -> Calculator.divide(0, 1));
}
@Test void groupedAssertions() {
var person = new Object() {
final String firstName = "John";
final String lastName = "Doe";
final int age = 47;
};
assertAll(
() -> assertEquals("John", person.firstName),
() -> assertEquals("Doe", person.lastName),
() -> assertEquals(47, person.age)
);
}
}
E agora vamos escrever os mesmos testes em Kotlin, no arquivo app/src/test/kotlin/KotlinAssertionsDemo.kt
:
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
class KotlinAssertionsDemo {
@Test fun `expected exception testing`() {
val exception = assertThrows<ArithmeticException> {
Calculator.divide(1, 0)
}
assertEquals("/ by zero", exception.message)
}
@Test fun `exception absence testing`() {
assertDoesNotThrow { Calculator.divide(0, 1) }
}
@Test fun `grouped assertions`() {
val person = object {
val firstName = "John"
val lastName = "Doe"
val age = 47
}
assertAll(
{ assertEquals("John", person.firstName) },
{ assertEquals("Doe", person.lastName) },
{ assertEquals(47, person.age) },
)
}
}
Vamos colocar os dois códigos lado a lado para comparação:
Com relação aos assertions usados no Kotlin, podemos destacar algumas diferenças:
Na chamada do
assertThrows
, o tipo da exception esperada é especificado fora dos parênteses, logo após o nome do método. Essa sintaxe é utilizada com funções que suportam reified type parameters.[33]Nas chamadas do
assertThrows
eassertDoesNotThrow
, não é necessário o uso de parêntesis para passagem dos parâmetros. Basta passarmos diretamente um function literal, definido entre{}
.[34]Na chamada do
assertAll
, fica evidente a diferença de sintaxe de lambda expressions do Kotlin e do Java.[34][35]
Entrar em detalhes destes três conceitos citados acima está além do escopo deste artigo. Caso queira se aprofundar, é possível consultar as documentações do Kotlin ou do Java. Lambda Expressions estão disponíveis tanto no Kotlin quanto no Java, desde a versão 8.[34][35][36] Enquanto reified type parameters e function literals são recursos exclusivos do Kotlin.[33][34]
6. Resumo
-
Para criar a estrutura mínima de um projeto Gradle do zero, use o plugin Build Init do Gradle:
gradle init \ --type basic \ --dsl kotlin \ --no-comments \ --use-defaults
-
Para configurar o Kotlin e o JUnit no projeto, defina as versões das ferramentas no catálogo de versões do Gradle:
[versions] junit-bom = "5.12.0" kotlin = "2.1.10" [libraries] junit-bom = { module = "org.junit:junit-bom", version.ref = "junit-bom" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
-
Em seguida, configure o projeto Gradle, fazendo referência ao catálogo de versões:
import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent plugins { alias(libs.plugins.kotlin.jvm) } repositories { mavenCentral() } dependencies { testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) testRuntimeOnly(libs.junit.platform.launcher) } tasks.test { useJUnitPlatform() testLogging { events( TestLogEvent.FAILED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.STANDARD_OUT ) exceptionFormat = TestExceptionFormat.FULL } }
-
Feitas estas configurações, desenvolva o primeiro método de teste com o JUnit:
class JUnitJupiterTests { @Test fun `test something`() { assertEquals(2, 1 + 1) } }
7. Conclusão
Kotlin, JUnit 5 e Gradle, são ferramentas modernas e que funcionam muito bem em conjunto, oferecendo funcionalidades que atendem a diversos cenários de automação de testes funcionais. Graças a interoperabilidade com o Java, o Kotlin, apesar de ser mais moderno e recente, se beneficiou da compatibilidade com ferramentas maduras e com uso já consolidado em aplicações na JVM, como o JUnit e o Gradle.
A utilização do SDKMAN! como gerenciador de versões do JDK e do Gradle auxilia na configuração do ambiente de desenvolvimento, possibilitando instalar as versões desejadas de cada uma das ferramentas. Além disso, ele permite alternar, facilmente, entre versões de diferentes ferramentas utilizando uma interface de comandos unificada.
Usando o Gradle como gerenciador do projeto, iniciamos do zero um projeto de automação de testes, seguindo uma estrutura de diretórios amplamente utilizada em projetos para a JVM. Além disso, a utilização do plugin Init do Gradle resultou em uma estrutura de projeto alinhada com as funcionalidades e recomendações mais recentes do Gradle.
Ao iniciar a configuração do projeto no IntelliJ, vimos como ele se integra facilmente ao ambiente de desenvolvimento que configuramos pelo terminal. Sua interface gráfica oferece suporte ao Kotlin e ao Gradle, facilitando no desenvolvimento e execução dos testes.
O sistema de catálogo de versões do Gradle permite centralizar as versões de todas as dependências do projeto. À medida em que um projeto cresce, novas dependências podem facilmente ser adicionadas no catálogo, ficando disponíveis para todos os subprojetos existentes.
Para configurar o JUnit, a utilização do Bill of Materials simplifica o ajuste de compatibilidade entre versões de diferentes dependências do JUnit. E para fazer uma atualização do JUnit, basta atualizarmos a versão do junit-bom
.
Através do modelo de programação oferecido pelo JUnit, baseado em annotations e assertions, conseguimos desenvolver casos de teste para diversos cenários e necessidades, organizando uma suíte de testes em métodos, classes e packages do Kotlin. Devido a interoperabilidade do Kotlin com o Java, podemos tirar proveito de um framework amplamente utilizado como o JUnit, combinado com a sintaxe moderna e concisa do Kotlin, resultando, em alguns casos, em testes relativamente mais limpos e menos verbosos.
A legibilidade dos testes é um fator fundamental para a manutenção de uma suíte de testes automatizados, e vimos que o Kotlin e o JUnit oferecem recursos que contribuem para isto. Todo o código desenvolvido neste artigo pode ser encontrado neste repositório do GitHub.
8. Referências
- Getting Started with Kotlin. Kotlin Documentation. Acesso em: 23 mar. 2025.
- What is JUnit 5?. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Gradle Build Tool. Gradle User Manual. Acesso em: 23 mar. 2025.
- Installing Gradle. Gradle User Manual. Acesso em: 23 mar. 2025.
- Get started with Kotlin/JVM. Kotlin Documentation. Acesso em: 23 mar. 2025.
- The Software Development Kit Manager. SDKMAN!. Acesso em: 23 mar. 2025.
- Project structure settings. IntelliJ Documentation. Acesso em: 23 mar. 2025.
- Plugin Basics. Gradle User Manual. Acesso em: 23 mar. 2025.
- Version Catalogs. Gradle User Manual. Acesso em: 23 mar. 2025.
- Configure a Gradle project. Kotlin Documentation. Acesso em: 23 mar. 2025.
- Using Plugins. Gradle User Manual. Acesso em: 23 mar. 2025.
- Object declarations and expressions. Kotlin Documentation. Acesso em: 23 mar. 2025.
- JUnit Jupiter. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- JUnit Platform Launcher API. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Aligning dependency versions. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Importing a platform. Gradle User Manual. Acesso em: 23 mar. 2025.
- Using JUnit 5. Gradle User Manual. Acesso em: 23 mar. 2025.
- Relying on automatic test framework implementation dependencies. Gradle User Manual. Acesso em: 23 mar. 2025.
- Test logging. Gradle User Manual. Acesso em: 23 mar. 2025.
- Test. Gradle User Manual. Acesso em: 23 mar. 2025.
- TestLoggingContainer. Gradle User Manual. Acesso em: 23 mar. 2025.
- Annotations. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Parameterized Tests. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- @CsvSource. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Test filtering. Gradle User Manual. Acesso em: 23 mar. 2025.
- Customizing Display Names. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Test Instance Lifecycle. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- @MethodSource. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Companion objects. Kotlin Documentation. Acesso em: 23 mar. 2025.
- Static methods. Kotlin Documentation. Acesso em: 23 mar. 2025.
- Sources of Arguments. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Assertions. JUnit 5 User Guide. Acesso em: 23 mar. 2025.
- Reified type parameters. Kotlin Documentation. Acesso em: 23 mar. 2025.
- Lambda expressions and anonymous functions. Kotlin Documentation. Acesso em: 23 mar. 2025.
- Lambda Expressions. The Java™ Tutorials. Acesso em: 23 mar. 2025.
- What's New in JDK 8. Java SE. Acesso em: 23 mar. 2025.