Ao concluirmos esta aula, nós:
- Seremos capazes de escrever asserções expressivas com JUnit Jupiter 6 e Hamcrest em Kotlin.
- Compreenderemos quando devemos utilizar Hamcrest (especialmente ao testar controladores Spring MVC com
MockMvc).
Configurando as Dependências Necessárias
Antes de podermos escrever asserções com o Hamcrest, precisamos garantir que a biblioteca esteja disponível no classpath de teste. No ecossistema moderno (Hamcrest 2.x / 3.x), o artefato unificado é o org.hamcrest:hamcrest (que inclusive já vem incluído transitivamente no 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.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
|
No Gradle 9 (Kotlin DSL), declaramos a dependência dentro da nossa suíte de testes 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.hamcrest:hamcrest:3.0")
}
}
}
}
|
Depois de adicionarmos essa dependência ao nosso script de build, podemos começar a escrever asserções com o Hamcrest.
Quando queremos escrever asserções com o Hamcrest no JUnit Jupiter 6, utilizamos o método estático assertThat() da classe org.hamcrest.MatcherAssert (que substituiu o antigo org.junit.Assert.assertThat depreciado no JUnit 4).
Esse método recebe até três parâmetros na seguinte ordem:
- Um parâmetro opcional (
String) contendo a mensagem de erro customizada que será exibida caso a asserção falhe.
- O valor ou objeto real obtido (
actual).
- O matcher do Hamcrest (
Matcher<T>) que especifica a condição ou valor esperado (expected).
Nota sobre a Palavra Reservada is no Kotlin:
Em Kotlin, is é uma palavra-chave reservada da linguagem (usada para checagem de tipos, equivalente ao instanceof do Java). Por isso, ao usar o matcher Matchers.is(...) do Hamcrest em Kotlin, você pode:
1. Envolver o método entre crases: `is`(expected);
2. Usar seu método equivalente no Hamcrest: equalTo(expected);
3. Ou usar um import alias do Kotlin no topo do arquivo: import org.hamcrest.Matchers.`is` as isEqualTo.
Vamos avançar e examinar exemplos práticos que demonstram como escrever diferentes tipos de asserções com Hamcrest em Kotlin.
Asserções para Valores Booleanos
Se quisermos verificar que um valor Boolean é true, criamos o matcher apropriado invocando o método `is`(true) da classe Matchers. Em outras palavras, podemos usar uma destas duas asserções:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.Test
class BooleanAssertionTest {
companion object {
private const val TRUE = true
}
@Test
fun booleanShouldBeTrue() {
assertThat(TRUE, `is`(true))
assertThat(
"Expected boolean to be true but was false",
TRUE,
`is`(true)
)
}
}
|
Se quisermos verificar que um valor Boolean é false, invocamos `is`(false) da classe Matchers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.Test
class BooleanAssertionTest {
companion object {
private const val FALSE = false
}
@Test
fun booleanShouldBeFalse() {
assertThat(FALSE, `is`(false))
assertThat(
"Expected boolean to be false but was true",
FALSE,
`is`(false)
)
}
}
|
Asserções para Verificar se um Objeto É null ou Não É null
Se quisermos verificar que um objeto é null, criamos o matcher do Hamcrest invocando o método nullValue() da classe Matchers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.Test
class ObjectAssertionTest {
@Test
fun objectShouldBeNull() {
val actual: Any? = null
assertThat(actual, nullValue())
assertThat(
"Expected object to be null but was: $actual",
actual,
nullValue()
)
}
}
|
Se quisermos verificar que um objeto não é null, criamos o matcher invocando o método notNullValue() da classe Matchers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.notNullValue
import org.junit.jupiter.api.Test
class ObjectAssertionTest {
@Test
fun objectShouldNotBeNull() {
val actual: Any? = Any()
assertThat(actual, notNullValue())
assertThat(
"Expected object to be not null but was null",
actual,
notNullValue()
)
}
}
|
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, invocamos o método `is`() (ou equalTo()) da classe Matchers. Por exemplo, para comparar dois números inteiros (Int), usamos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.jupiter.api.Test
class ObjectAssertionTest {
@Test
fun twoIntegersShouldBeEqual() {
val actual = 9
val expected = 9
assertThat(actual, `is`(expected))
assertThat(
"Expected integer to be: $expected but was: $actual",
actual,
`is`(expected)
)
}
}
|
Se quisermos verificar que o valor real obtido não é igual ao valor esperado, invocamos o método not() da classe Matchers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.junit.jupiter.api.Test
class ObjectAssertionTest {
@Test
fun twoIntegersShouldNotBeEqual() {
val actual = 9
val expected = 41
assertThat(actual, not(expected))
assertThat(
"Expected integer to not be: $expected but was: $actual",
actual,
not(expected)
)
}
}
|
Asserções de Referência de Objetos
Se quisermos verificar que duas variáveis referenciam exatamente a mesma instância de objeto na memória, criamos o matcher invocando o método sameInstance() da classe Matchers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.sameInstance
import org.junit.jupiter.api.Test
class ObjectAssertionTest {
@Test
fun shouldPointToSameObject() {
val actual = Any()
val expected = actual
assertThat(actual, sameInstance(expected))
assertThat(
"Expected two objects to be the same but they are not",
actual,
sameInstance(expected)
)
}
}
|
Nota: Para verificar que dois objetos não apontam para a mesma instância no Hamcrest, você pode combinar os matchers not(sameInstance(expected)).
Asserções de Igualdade entre Dois Arrays
Se quisermos verificar que dois arrays são iguais, podemos invocar o método `is`() da classe Matchers. Por exemplo, para verificar que dois IntArray possuem os mesmos elementos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
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, `is`(expected))
assertThat(
"Expected array: ${expected.contentToString()} but got array: ${actual.contentToString()}",
actual,
`is`(expected)
)
}
}
|
Asserções para Listas (List)
Se quisermos escrever uma asserção que verifica se o tamanho (size) de uma lista está correto, criamos o matcher invocando o método hasSize() da classe Matchers. Por exemplo, para verificar que o tamanho de uma lista vazia é 0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.junit.jupiter.api.Test
class ListAssertionTest {
@Test
fun sizeOfEmptyListShouldBeZero() {
val emptyList = emptyList<String>()
assertThat(emptyList, hasSize(0))
assertThat(
"Expected size to be: 0 but was: ${emptyList.size}",
emptyList,
hasSize(0)
)
}
}
|
Se quisermos escrever uma asserção que verifica se uma lista contém os objetos esperados, escolhemos um destes dois matchers do Hamcrest:
- Para verificar que a lista contém os objetos esperados na ordem exata especificada, invocamos o método
contains() da classe Matchers.
- Para verificar que a lista contém os objetos esperados em qualquer ordem, invocamos o método
containsInAnyOrder() da classe Matchers.
A classe de teste que valida ambas as situações fica assim:
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
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.contains
import org.hamcrest.Matchers.containsInAnyOrder
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, contains(first, second))
}
@Test
fun listShouldContainCorrectObjectsInAnyOrder() {
assertThat(list, containsInAnyOrder(second, first))
}
}
|
Asserções para Mapas (Map)
Se quisermos verificar que um mapa contém uma chave específica, criamos o matcher invocando o método hasKey() da classe Matchers:
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
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasKey
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class MapAssertionTest {
companion object {
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, hasKey(KEY))
}
}
|
Se quisermos verificar que um mapa contém um par chave-valor específico (entry), invocamos o método hasEntry() da classe Matchers:
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
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasEntry
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class MapAssertionTest {
companion object {
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, hasEntry(KEY, expected))
}
}
|
Combinando Múltiplas Asserções do Hamcrest
Quando queremos construir uma asserção composta por múltiplos matchers do Hamcrest, podemos escolher uma destas duas opções:
- Se quisermos garantir que todas as condições sejam satisfeitas simultaneamente (operador lógico
AND), invocamos o método allOf() da classe Matchers.
- Se quisermos garantir que pelo menos uma das condições seja satisfeita (operador lógico
OR), invocamos o método anyOf() da classe Matchers.
Os exemplos a seguir demonstram como combinar múltiplos matchers do Hamcrest ao escrever asserções para um objeto Task. Esses exemplos utilizam o matcher hasProperty(), que recebe dois parâmetros:
- O nome da propriedade (que possui um getter JavaBeans correspondente, como
getId(), getTitle()).
- O matcher do Hamcrest que inspecionará o valor daquela propriedade.
Se quisermos verificar que todos os valores de propriedades do objeto Task criado estão corretos, escrevemos o seguinte método de teste:
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
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.hasProperty
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class TaskTest {
companion object {
private const val ID = 1L
private const val ASSIGNEE_ID = 99L
private const val CREATOR_ID = 44L
private const val TITLE = "Write an example project"
private const val DESCRIPTION = "description"
}
private lateinit var task: Task
@BeforeEach
fun createTask() {
task = Task.getBuilder()
.withId(ID)
.withAssignee(ASSIGNEE_ID)
.withCreator(CREATOR_ID)
.withTitle(TITLE)
.withDescription(DESCRIPTION)
.build()
}
@Test
fun build_shouldCreateTaskWithCorrectFieldValues() {
assertThat(
task,
allOf(
hasProperty("id", `is`(ID)),
hasProperty("assignee", hasProperty("userId", `is`(ASSIGNEE_ID))),
hasProperty("closer", nullValue()),
hasProperty("creator", hasProperty("userId", `is`(CREATOR_ID))),
hasProperty("title", `is`(TITLE)),
hasProperty("description", `is`(DESCRIPTION)),
hasProperty("status", `is`(TaskStatus.OPEN)),
hasProperty("resolution", nullValue())
)
)
}
}
|
Por outro lado, se quisermos verificar que pelo menos um dos campos do objeto Task corresponde às condições listadas, utilizamos anyOf():
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
|
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.anyOf
import org.hamcrest.Matchers.hasProperty
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.nullValue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class TaskTest {
companion object {
private const val ID = 1L
private const val ASSIGNEE_ID = 99L
private const val CREATOR_ID = 44L
private const val TITLE = "Write an example project"
private const val DESCRIPTION = "description"
}
private lateinit var task: Task
@BeforeEach
fun createTask() {
task = Task.getBuilder()
.withId(ID)
.withAssignee(ASSIGNEE_ID)
.withCreator(CREATOR_ID)
.withTitle(TITLE)
.withDescription(DESCRIPTION)
.build()
}
@Test
fun build_shouldCreateTaskWithAtLeastOneCorrectFieldValue() {
assertThat(
task,
anyOf(
hasProperty("id", `is`(ID)),
hasProperty("assignee", hasProperty("userId", `is`(ASSIGNEE_ID))),
hasProperty("closer", nullValue()),
hasProperty("creator", hasProperty("userId", `is`(CREATOR_ID))),
hasProperty("title", `is`(TITLE)),
hasProperty("description", `is`(DESCRIPTION)),
hasProperty("status", `is`(TaskStatus.OPEN)),
hasProperty("resolution", nullValue())
)
)
}
}
|
Agora que sabemos como escrever diversas asserções utilizando matchers do Hamcrest, vamos analisar quando devemos aplicar essa técnica.
Quando Devemos Usar o Hamcrest?
Os exemplos acima demonstram que escrever asserções com o Hamcrest é uma escolha muito superior às asserções padrão do JUnit (assertEquals) por três razões:
- Passamos os valores na ordem natural de raciocínio: primeiro o valor obtido (
actual), depois o matcher esperado (expected).
- Os matchers do Hamcrest fazem com que as asserções sejam lidas quase como uma frase em inglês natural (
assertThat(actual, is(expected))), facilitando a leitura do teste.
- O método
assertThat(T actual, Matcher<? super T> matcher) protege o desenvolvedor contra inversões acidentais: como o segundo parâmetro exige um tipo Matcher, é impossível inverter actual e expected sem que o compilador aponte erro imediatamente!
Em testes unitários puros de domínio em Kotlin, costumamos preferir o AssertJ (que veremos a partir da aula 04 deste tópico) ou Kotest Matchers porque o autocompletar fluente (.) na IDE é ainda mais produtivo do que importar dezenas de métodos estáticos da classe Matchers.
No entanto, conhecer bem o Hamcrest é indispensável no ecossistema Spring: quando escrevemos testes para controladores web e APIs REST utilizando o MockMvc (Spring MVC Test framework), os métodos de verificação de JSON (jsonPath("$.title", is("..."))), modelo (model().attribute(...)) e conteúdo utilizam matchers do Hamcrest nativamente!
Vamos resumir o que aprendemos nesta aula.
Resumo
Esta aula nos ensinou quatro pontos principais:
- Para escrever asserções com Hamcrest no JUnit 6, utilizamos o método
MatcherAssert.assertThat().
- Para combinar múltiplos matchers do Hamcrest em uma única verificação, usamos os métodos
allOf() (E lógico) ou anyOf() (OU lógico) da classe Matchers.
- Escrever asserções com Hamcrest é muito mais seguro e legível do que usar
assertEquals(expected, actual) da API básica do JUnit.
- Os matchers do Hamcrest são amplamente utilizados em conjunto com o
MockMvc ao testar endpoints web e JSON no Spring MVC Test Framework.