Escrevendo Asserções Básicas com JUnit Jupiter

Ao concluirmos esta aula, nós:

  • Seremos capazes de escrever asserções utilizando a API padrão do JUnit Jupiter (org.junit.jupiter.api.Assertions).
  • Compreenderemos como a ordem dos parâmetros evoluiu do JUnit 4 (org.junit.Assert) para o JUnit Jupiter 6 e como usar avaliação tardia (lazy) de mensagens em Kotlin.
  • Compreenderemos por que, apesar dessas melhorias, ainda preferimos bibliotecas fluentes (como AssertJ) no dia a dia.

Escrevendo Asserções com a API Padrão do JUnit Jupiter

Se quisermos escrever asserções utilizando a API padrão embutida no JUnit Jupiter 6, devemos utilizar a classe org.junit.jupiter.api.Assertions (que substituiu a antiga classe org.junit.Assert do JUnit 4). Ela fornece diversos métodos estáticos para verificarmos resultados em nossos testes.

Antes de examinarmos cada método em detalhes, precisamos conhecer três regras fundamentais:

  • Ordem dos valores comparados: Quando comparamos dois valores (ou objetos), devemos passá-los ao método de asserção sempre nesta ordem: primeiro o valor esperado (expected) e depois o valor real obtido (actual).
  • Posição da mensagem de erro customizada: No antigo JUnit 4 (org.junit.Assert), a mensagem de erro customizada (String) era passada como o primeiro parâmetro do método. No JUnit Jupiter 6 (Assertions), isso foi corrigido e padronizado: a mensagem de erro opcional é sempre o último parâmetro do método!
  • Suporte a Lambdas (Avaliação Lazy): No JUnit Jupiter 6, o último parâmetro pode ser tanto uma String quanto um lambda () -> String. Em Kotlin, passar um lambda { "mensagem..." } garante que a string de erro só será construída na memória se a asserção realmente falhar!

Vamos descobrir que tipos de asserções podemos escrever com a classe Assertions.

Asserções para Valores Booleanos

Se quisermos verificar que um valor Boolean é true, devemos utilizar o método assertTrue() da classe Assertions. Em outras palavras, podemos usar qualquer uma destas variações:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class BooleanAssertionTest {

    companion object {
        private const val TRUE = true
    }

    @Test
    fun booleanShouldBeTrue() {
        assertTrue(TRUE)
        assertTrue(TRUE, "TRUE should be true but was false")
        assertTrue(TRUE) { "TRUE should be true but was false" }
    }
}

Se quisermos verificar que um valor Boolean é false, devemos utilizar o método assertFalse() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Test

class BooleanAssertionTest {

    companion object {
        private const val FALSE = false
    }

    @Test
    fun booleanShouldBeFalse() {
        assertFalse(FALSE)
        assertFalse(FALSE, "FALSE should be false but was true")
        assertFalse(FALSE) { "FALSE should be false but was true" }
    }
}

Asserções para Verificar se um Objeto É null ou Não É null

Se quisermos verificar que um objeto é null, devemos utilizar o método assertNull() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun nullShouldBeNull() {
        val actual: Any? = null

        assertNull(actual)
        assertNull(actual) {
            "Expected actual to be null but was: $actual"
        }
    }
}

Se quisermos verificar que um objeto não é null, devemos utilizar o método assertNotNull() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun objectShouldNotBeNull() {
        val actual: Any? = Any()

        assertNotNull(actual)
        assertNotNull(actual, "Expected actual to be not null but was null")
    }
}

Asserções de Igualdade entre Dois Valores ou Objetos

Se quisermos verificar que o valor (ou objeto) esperado é igual ao valor (ou objeto) real obtido (via método equals()), devemos usar o método assertEquals() da classe Assertions. Por exemplo, para comparar dois números inteiros (Int), podemos usar uma destas asserções:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun twoIntegersShouldBeEqual() {
        val actual = 9
        val expected = 9

        assertEquals(expected, actual)
        assertEquals(expected, actual) {
            "Expected actual to be: $expected but was: $actual"
        }
    }
}

Se quisermos verificar que o valor (ou objeto) esperado não é igual ao valor (ou objeto) real obtido, devemos usar o método assertNotEquals() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun twoIntegersShouldNotBeEqual() {
        val actual = 9
        val expected = 4

        assertNotEquals(expected, actual)
        assertNotEquals(expected, actual) {
            "Expected actual to not be: $expected but was: $actual"
        }
    }
}

Asserções de Referência de Objetos (Identidade em Memória)

Se quisermos garantir que duas variáveis apontam exatamente para a mesma instância de objeto na memória (=== em Kotlin), devemos utilizar o método assertSame() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun shouldPointToSameObject() {
        val actual = Any()
        val expected = actual

        assertSame(expected, actual)
        assertSame(expected, actual, "Expected two objects to be the same but they are not")
    }
}

Se quisermos garantir que duas variáveis não apontam para a mesma instância na memória (!== em Kotlin), devemos utilizar o método assertNotSame() da classe Assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import org.junit.jupiter.api.Assertions.assertNotSame
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

    @Test
    fun shouldNotPointToSameObject() {
        val actual = Any()
        val expected = Any()

        assertNotSame(expected, actual)
        assertNotSame(expected, actual, "Expected two objects to not be the same but they are")
    }
}

Asserções de Igualdade entre Dois Arrays

Se quisermos verificar que dois arrays possuem exatamente os mesmos elementos na mesma ordem, devemos usar o método assertArrayEquals() da classe Assertions. Por exemplo, para comparar dois arrays de inteiros (IntArray), usamos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Test

class ArrayAssertionTest {

    @Test
    fun twoIntArraysShouldBeEqual() {
        val actual = intArrayOf(2, 5, 7)
        val expected = intArrayOf(2, 5, 7)

        assertArrayEquals(expected, actual)
        assertArrayEquals(expected, actual) {
            "Expected array: ${expected.contentToString()} to be equal to array: ${actual.contentToString()} but it is not"
        }
    }
}

Agora já sabemos como escrever diferentes tipos de asserções utilizando a API padrão do JUnit. Vamos entender por que, mesmo com as melhorias do JUnit Jupiter 6, não costumamos utilizá-la como nossa biblioteca principal de asserções.


Por Que Não Devemos Usar as Asserções “Padrão” do JUnit?

As asserções padrão do JUnit possuem dois problemas ergonômicos importantes:

  • Precisamos passar ambos os valores (expected e actual) como parâmetros dentro do mesmo método estático (assertEquals(expected, actual)), sem assistência de autocompletar contextual da IDE específica para o tipo do objeto.
  • Precisamos passar os parâmetros em uma ordem que contraria o fluxo natural de leitura (expected antes de actual).

Uma das características mais importantes de uma boa API é proteger seus usuários contra erros acidentais de programação. Infelizmente, a API clássica assertEquals(expected, actual) falha nesse quesito.

Deixe-me explicar o motivo.

Como o método recebe os dois valores do mesmo tipo como parâmetros (assertEquals(a, b)), é crucial que a ordem dos argumentos seja natural para quem escreve o código. O problema é que, quando pensamos no teste, nosso cérebro raciocina na ordem: primeiro o valor obtido (actual), depois o valor esperado (expected).

A melhor maneira de demonstrar a diferença é comparar estas duas frases:

  1. O valor Y é retornado quando invocamos o método X.
  2. Quando invocamos o método X, o valor Y é retornado.

A primeira frase descreve a ordem exigida pela API clássica do JUnit (assertEquals(expected, actual)), enquanto a segunda frase descreve a ordem fluente e natural que queremos usar (assertThat(actual).isEqualTo(expected)).

Se você concorda que a segunda frase é muito mais natural, provavelmente já percebeu que usar assertEquals(expected, actual) exige atenção constante para não inverter os dois parâmetros. Mais cedo ou mais tarde, todo desenvolvedor acaba invertendo expected e actual por engano — e quando o teste falha, a mensagem de erro no console diz exatamente o oposto do que aconteceu (Expected: <valor errado> but was: <valor esperado>), causando enorme confusão!

Nota: Se você acha que a primeira frase faz mais sentido do que a segunda, você pode usar a API padrão Assertions do JUnit Jupiter sem problemas. Contudo, como a leitura fluente da esquerda para a direita (actual -> expected) é muito mais legível e segura, as demais aulas deste curso utilizarão bibliotecas fluentes como Hamcrest, AssertJ e Kotest Matchers.

Vamos resumir o que aprendemos nesta aula.


Resumo

Esta aula nos ensinou três pontos principais:

  • Se quisermos escrever asserções usando a API nativa do JUnit Jupiter 6, utilizamos os métodos da classe org.junit.jupiter.api.Assertions (substituta da antiga org.junit.Assert do JUnit 4), onde a mensagem de erro customizada passa a ser sempre o último argumento (suportando lambdas em Kotlin).
  • Aprendemos a verificar booleanos (assertTrue/assertFalse), nulidade (assertNull/assertNotNull), igualdade (assertEquals/assertNotEquals), referências (assertSame/assertNotSame) e arrays (assertArrayEquals).
  • Evitamos usar assertEquals(expected, actual) em projetos reais porque passar o valor esperado antes do valor real obtido é propenso a inversões acidentais e menos expressivo que APIs fluentes.