Escrevendo Asserções Fluentes com AssertJ 3.27

Ao concluirmos esta aula, nós:

  • Seremos capazes de escrever asserções fluentes e encadeadas com o AssertJ 3.27 em Kotlin.
  • Compreenderemos por que devemos utilizar o AssertJ como nossa biblioteca padrão de asserções em testes JUnit Jupiter.

Configurando as Dependências Necessárias

Antes de podermos escrever asserções com o AssertJ, precisamos garantir que a dependência assertj-core esteja declarada em nosso script de build. Na stack moderna com Java 21+ / Kotlin 2.4 e Spring Boot 4.1, utilizamos a série 3.27.x (que já vem incluída automaticamente quando usamos spring-boot-starter-test).

Se estivermos usando Maven em um projeto isolado, declaramos a dependência no arquivo pom.xml:

1
2
3
4
5
6
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.27.7</version>
    <scope>test</scope>
</dependency>

Se estivermos usando Gradle 9 (Kotlin DSL), declaramos a dependência no arquivo build.gradle.kts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter("6.0.3")
            dependencies {
                implementation("org.assertj:assertj-core:3.27.7")
            }
        }
    }
}

Depois de adicionarmos essa dependência ao nosso script de build, podemos começar a escrever asserções com o AssertJ.


Escrevendo Asserções com AssertJ

Quando queremos escrever asserções com o AssertJ, utilizamos o método estático assertThat() da classe org.assertj.core.api.Assertions. Esse método recebe o valor ou objeto real obtido (actual) como seu único parâmetro e retorna um objeto de asserção fortemente tipado específico para o tipo de actual. A partir do retorno de assertThat(actual), basta digitar . na IDE para descobrir todos os métodos de verificação disponíveis para aquele tipo!

Como o AssertJ fornece uma API fluente, cada método de asserção retorna uma referência para o próprio “objeto de asserção”. Isso significa que podemos encadear múltiplas asserções sobre o mesmo alvo de forma contínua e legível.

Por exemplo, se quisermos escrever uma asserção que verifica simultaneamente se o tamanho de uma lista está correto e se a lista contém os objetos esperados na ordem exata, escrevemos:

 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ListAssertionTest {

    companion object {
        private const val EXPECTED_SIZE = 2
    }

    private lateinit var first: Any
    private lateinit var second: Any
    private lateinit var list: List<Any>

    @BeforeEach
    fun createAndInitializeList() {
        first = Any()
        second = Any()
        list = listOf(first, second)
    }

    @Test
    fun sizeOfListShouldBeTwoAndItShouldContainCorrectObjects() {
        assertThat(list)
            .hasSize(EXPECTED_SIZE)
            .containsExactly(first, second)
    }
}

Se quisermos fornecer uma mensagem de erro customizada que será exibida quando nossa asserção falhar, podemos escolher uma destas duas opções da classe base AbstractAssert:

  • Se quisermos sobrescrever apenas o prefixo/descrição da mensagem de erro (mantendo os detalhes do Expected e Actual gerados pelo AssertJ), invocamos o método describedAs() (ou `as`() entre crases em Kotlin, pois as é palavra reservada de cast no Kotlin) antes do método de verificação.
  • Se quisermos sobrescrever a mensagem de erro inteira, invocamos o método overridingErrorMessage() (ou withFailMessage()) antes do método de verificação.

O código-fonte a seguir demonstra o uso de ambas as opções:

 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class BooleanAssertionTest {

    companion object {
        private const val TRUE = true
    }

    @Test
    fun booleanShouldBeTrueWithDescribedAs() {
        assertThat(TRUE)
            .describedAs("Expected boolean to be true but was false")
            .isTrue()
    }

    @Test
    fun booleanShouldBeTrueWithOverridingErrorMessage() {
        assertThat(TRUE)
            .overridingErrorMessage(
                "Expected boolean to be true but was false"
            )
            .isTrue()
    }
}

Veja a diferença prática na saída quando cada uma dessas asserções falha:

Se a asserção que invoca describedAs() falhar, veremos a descrição entre colchetes seguida pela comparação padrão:

1
2
org.opentest4j.AssertionFailedError: [Expected boolean to be true but was false] 
Expecting value to be false but was true

Já se a asserção que invoca overridingErrorMessage() falhar, veremos exatamente a mensagem customizada que definimos:

1
java.lang.AssertionError: Expected boolean to be true but was false
Nota: Quando quero fornecer uma mensagem de erro totalmente personalizada, costumo usar overridingErrorMessage() porque ela substitui toda a mensagem de erro. No entanto, se você preferir apenas adicionar contexto mantendo o comparativo padrão do AssertJ, sinta-se à vontade para usar describedAs()!

Vamos avançar e examinar exemplos que demonstram como escrever asserções simples com o AssertJ para os tipos mais comuns.

Asserções para Valores Booleanos

Se quisermos verificar que um valor Boolean é true, invocamos o método isTrue() da classe AbstractBooleanAssert:

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

class BooleanAssertionTest {

    companion object {
        private const val TRUE = true
    }

    @Test
    fun booleanShouldBeTrue() {
        assertThat(TRUE)
            .overridingErrorMessage(
                "Expected boolean to be true but was false"
            )
            .isTrue()
    }
}

Se quisermos verificar que um valor Boolean é false, invocamos o método isFalse() da classe AbstractBooleanAssert:

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

class BooleanAssertionTest {

    companion object {
        private const val FALSE = false
    }

    @Test
    fun booleanShouldBeFalse() {
        assertThat(FALSE)
            .overridingErrorMessage(
                "Expected boolean to be false but was true"
            )
            .isFalse()
    }
}

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

Se quisermos verificar que um objeto é null, invocamos o método isNull() da classe AbstractAssert:

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

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected object to be null but was: %s",
                actual
            )
            .isNull()
    }
}

Se quisermos verificar que um objeto não é null, invocamos o método isNotNull() da classe AbstractAssert:

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

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected object to be not null but was null"
            )
            .isNotNull()
    }
}

Asserções de Igualdade entre Dois Valores ou Objetos

Se quisermos verificar que o valor (ou objeto) real obtido é igual ao valor (ou objeto) esperado, podemos invocar tanto o método isEqualTo() da classe AbstractAssert quanto o método isEqualByComparingTo() da classe AbstractComparableAssert. A diferença entre eles é crucial:

  • O método isEqualTo() verifica igualdade invocando o método equals().
  • O método isEqualByComparingTo() verifica equivalência invocando o método compareTo() da interface Comparable (muito útil para BigDecimal, onde new BigDecimal("10.0") e new BigDecimal("10.00") têm equals() falso devido à escala, mas compareTo() == 0 verdadeiro!).

Para verificar que dois números inteiros (Int) são iguais:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected integer to be: %d but was: %d",
                expected,
                actual
            )
            .isEqualByComparingTo(expected)
    }
}

Se quisermos verificar que o valor real obtido não é igual ao valor esperado, invocamos isNotEqualTo() ou isNotEqualByComparingTo():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected integer to not be: %d but was %d",
                expected,
                actual
            )
            .isNotEqualByComparingTo(expected)
    }
}

Asserções de Referência de Objetos

Se quisermos garantir que duas variáveis apontam exatamente para a mesma instância de objeto na memória (===), invocamos o método isSameAs() da classe AbstractAssert:

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

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected two objects to be same but they were not"
            )
            .isSameAs(expected)
    }
}

Se quisermos garantir que dois objetos não apontam para a mesma referência na memória (!==), invocamos o método isNotSameAs() da classe AbstractAssert:

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

class ObjectAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected two objects to not be same but they were"
            )
            .isNotSameAs(expected)
    }
}

Asserções de Igualdade entre Dois Arrays

Se quisermos verificar que dois arrays possuem exatamente os mesmos elementos na mesma ordem, invocamos o método isEqualTo() da classe AbstractArrayAssert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class ArrayAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected array: %s but got array: %s",
                expected.contentToString(),
                actual.contentToString()
            )
            .isEqualTo(expected)
    }
}

Se quisermos verificar que dois arrays não são iguais, invocamos o método isNotEqualTo() da classe AbstractArrayAssert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class ArrayAssertionTest {

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

        assertThat(actual)
            .overridingErrorMessage(
                "Expected array: %s to not be equal to array: %s",
                expected.contentToString(),
                actual.contentToString()
            )
            .isNotEqualTo(expected)
    }
}

Asserções para Listas (List)

Se quisermos verificar o tamanho de uma lista com o AssertJ, utilizamos uma destas duas opções da classe AbstractIterableAssert:

  • Para verificar que uma lista está vazia, invocamos o método isEmpty().
  • Para verificar que uma lista possui um tamanho específico, invocamos o método hasSize().

Veja o exemplo abaixo que valida ambas as situações:

 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ListAssertionTest {

    companion object {
        private const val EXPECTED_SIZE = 2
    }

    private lateinit var first: Any
    private lateinit var second: Any
    private lateinit var list: List<Any>

    @BeforeEach
    fun createAndInitializeList() {
        first = Any()
        second = Any()
        list = listOf(first, second)
    }

    @Test
    fun sizeOfEmptyListShouldBeZero() {
        val emptyList = emptyList<String>()

        assertThat(emptyList)
            .overridingErrorMessage(
                "Expected size of empty list to be: 0 but was: %d",
                emptyList.size
            )
            .isEmpty()
    }

    @Test
    fun sizeOfListShouldBeTwo() {
        assertThat(list)
            .overridingErrorMessage(
                "Expected size of list to be: %d but was: %d",
                EXPECTED_SIZE,
                list.size
            )
            .hasSize(EXPECTED_SIZE)
    }
}

Se quisermos garantir que uma lista contém ou não contém objetos específicos, o AssertJ oferece métodos claros e diretos:

  • Para verificar que a lista contém apenas os objetos especificados na ordem exata, usamos containsExactly().
  • Para verificar que a lista contém apenas os objetos especificados em qualquer ordem, usamos containsExactlyInAnyOrder().
  • Para garantir que a lista não contém um objeto indesejado, invocamos doesNotContain().

Veja a implementação completa:

 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ListAssertionTest {

    private lateinit var first: Any
    private lateinit var second: Any
    private lateinit var list: List<Any>

    @BeforeEach
    fun createAndInitializeList() {
        first = Any()
        second = Any()
        list = listOf(first, second)
    }

    @Test
    fun listShouldContainCorrectObjectsInCorrectOrder() {
        assertThat(list).containsExactly(first, second)
    }

    @Test
    fun listShouldContainCorrectObjectsInAnyOrder() {
        assertThat(list).containsExactlyInAnyOrder(second, first)
    }

    @Test
    fun listShouldNotContainIncorrectObjects() {
        val incorrect = Any()
        assertThat(list).doesNotContain(incorrect)
    }
}

Asserções para Mapas (Map)

Se quisermos verificar se um mapa contém uma chave esperada ou se não contém uma chave incorreta, utilizamos os métodos da classe AbstractMapAssert:

  • Para verificar que o mapa contém a chave esperada, invocamos containsKey().
  • Para verificar que o mapa não contém uma chave incorreta, invocamos doesNotContainKey().
 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MapAssertionTest {

    companion object {
        private const val INCORRECT_KEY = "incorrectKey"
        private const val KEY = "key"
    }

    private lateinit var expected: Any
    private lateinit var map: MutableMap<String, Any>

    @BeforeEach
    fun createAndInitializeMap() {
        map = mutableMapOf()
        expected = Any()
        map[KEY] = expected
    }

    @Test
    fun shouldContainCorrectKey() {
        assertThat(map).containsKey(KEY)
    }

    @Test
    fun shouldNotContainIncorrectKey() {
        assertThat(map).doesNotContainKey(INCORRECT_KEY)
    }
}

Se quisermos verificar se um mapa contém uma entrada (entry chave-valor) específica ou se não contém uma entrada incorreta:

  • Para garantir que o mapa contém a entrada correta, invocamos containsEntry(key, value).
  • Para verificar que o mapa não contém uma entrada específica, invocamos doesNotContainEntry(key, value).
 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
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MapAssertionTest {

    companion object {
        private const val INCORRECT_KEY = "incorrectKey"
        private const val KEY = "key"
    }

    private lateinit var expected: Any
    private lateinit var map: MutableMap<String, Any>

    @BeforeEach
    fun createAndInitializeMap() {
        map = mutableMapOf()
        expected = Any()
        map[KEY] = expected
    }

    @Test
    fun shouldContainObjectWithCorrectKey() {
        assertThat(map).containsEntry(KEY, expected)
    }

    @Test
    fun shouldNotContainObjectWithIncorrectKey() {
        assertThat(map).doesNotContainEntry(INCORRECT_KEY, expected)
    }
}

Agora que dominamos as asserções fundamentais com o AssertJ, vamos entender por que devemos adotá-lo em vez das asserções padrão do JUnit ou do Hamcrest.


Por Que Devemos Usar o AssertJ?

Existem três razões decisivas para escrevermos nossas asserções com o AssertJ:

  • Asserções com AssertJ são muito mais rápidas de escrever do que com JUnit ou Hamcrest: Você só precisa importar um único método (Assertions.assertThat), passar o objeto real assertThat(actual) e a IDE lista automaticamente apenas os métodos compatíveis com aquele tipo (List, Map, String, Int), sem precisar lembrar nomes de matchers estáticos do Hamcrest.
  • O AssertJ protege seus usuários contra erros de programação: Como assertThat(actual) recebe apenas o valor obtido e retorna o objeto de asserção onde encadeamos .isEqualTo(expected), é praticamente impossível inverter a ordem entre actual e expected.
  • O AssertJ nos ajuda a escrever asserções naturais e encadeáveis: Ele segue perfeitamente a estrutura mental “Quando invocamos o método X, o valor Y é retornado” e possui métodos negativos nativos para todos os cenários (doesNotContain, doesNotContainKey, isNotSameAs), algo que faltava no Hamcrest básico.

Vamos comparar lado a lado as três abordagens para verificar a igualdade de dois inteiros:

 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
class ObjectAssertionComparisonTest {

    // 1. JUnit Jupiter Padrão
    @Test
    fun twoIntegersShouldBeEqualJUnit() {
        val actual = 9
        val expected = 9

        assertEquals(expected, actual)
    }

    // 2. Hamcrest
    @Test
    fun twoIntegersShouldBeEqualHamcrest() {
        val actual = 9
        val expected = 9

        assertThat(actual, `is`(expected))
    }

    // 3. AssertJ
    @Test
    fun twoIntegersShouldBeEqualAssertJ() {
        val actual = 9
        val expected = 9

        assertThat(actual).isEqualByComparingTo(expected)
    }
}

Tanto o Hamcrest quanto o AssertJ são muito mais fáceis de ler que o assertEquals(expected, actual) tradicional, mas a API fluente e fortemente tipada do AssertJ torna a experiência de escrita na IDE imbatível.


Resumo

Esta aula nos ensinou três pontos fundamentais:

  • No ecossistema moderno com Java 21+ e Kotlin 2.4, utilizamos o org.assertj:assertj-core:3.27.7.
  • Escrevemos asserções fluentes com o AssertJ invocando o ponto de entrada único org.assertj.core.api.Assertions.assertThat(actual).
  • Devemos priorizar o AssertJ em nossos testes porque sua API fluente previne erros de inversão de argumentos, suporta autocompletar contextual na IDE e gera código fácil de ler e manter.