Criando Matchers Customizados Reutilizáveis no Hamcrest
Ao concluirmos esta aula, nós:
- Saberemos como criar matchers customizados do Hamcrest em Kotlin.
- Seremos capazes de escolher a classe base ideal (
FeatureMatchervsTypeSafeDiagnosingMatcher) para cada tipo de verificação. - Saberemos como nomear nossos matchers e quando vale a pena criá-los.
Task).
Criando Matchers Customizados do Hamcrest
Quando queremos criar um matcher customizado do Hamcrest, precisamos criar um método fábrica (factory method) que instancia e configura o nosso matcher. Podemos adicionar esse método fábrica diretamente dentro da nossa classe de teste ou criar uma classe Object Mother dedicada para abrigá-lo, seguindo duas regras simples:
- Se o nosso matcher for utilizado por apenas uma classe de teste, devemos declarar esse método fábrica como
private funna própria classe de teste. - Se o nosso matcher for compartilhado por múltiplas classes de teste, devemos criar uma classe Object Mother (em Kotlin, um
object) e adicionar nosso método fábrica nela.
No Java clássico, uma classe Object Mother exigia criar uma classe public final com construtor private vazio para impedir instanciação. Em Kotlin, criamos um singleton nativo usando a palavra-chave object:
|
|
object que agrupa matchers customizados do Hamcrest, uma excelente convenção é adicionar o sufixo Matchers ao nome da classe de domínio inspecionada. Como nosso objeto cria matchers para inspecionar objetos Task, seu nome deve ser TaskMatchers.
Agora já temos onde colocar nossos métodos fábrica. Vamos avançar e criar primeiro um matcher customizado do Hamcrest que verifica se um único valor (propriedade) está correto.
Criando um Matcher Customizado do Hamcrest para um Único Valor (FeatureMatcher)
Se quisermos criar um matcher customizado do Hamcrest que verifica se uma única propriedade de um objeto está correta, devemos estender a classe abstrata org.hamcrest.FeatureMatcher<T, U>. Ao estender ou instanciar um FeatureMatcher<T, U>, fornecemos dois parâmetros de tipo genérico:
- O parâmetro de tipo
Tespecifica o tipo do objeto inspecionado (no nosso caso,Task). - O parâmetro de tipo
Uespecifica o tipo da propriedade (feature) extraída do objeto (por exemplo,String?para o título).
Vamos criar um matcher customizado que verifica se um objeto Task possui o título (title) esperado seguindo três passos:
Primeiro, adicionamos um novo método fábrica estático (@JvmStatic fun) dentro de TaskMatchers. Esse método retorna um FeatureMatcher<Task, String?> e recebe o título esperado (title: String) como parâmetro:
|
|
Segundo, modificamos nosso método fábrica para retornar uma instância anônima (object : FeatureMatcher<Task, String?>(...)) passando três argumentos ao construtor de FeatureMatcher:
- O matcher interno do Hamcrest usado para comparar o valor extraído com o valor esperado. Como queremos garantir que o título real seja igual ao título esperado, invocamos
`is`(title)da classeMatchers. - A descrição textual da propriedade inspecionada (exibida na linha
Expected:da mensagem de falha). Passaremos a string"title". - O nome da propriedade exibido na linha de divergência (
but:/ mismatch message) quando o matcher falha. Passaremos a string"title".
Depois de configurarmos o construtor do FeatureMatcher, o código-fonte de TaskMatchers fica assim:
|
|
Terceiro, sobrescrevemos o método featureValueOf(actual: Task) da classe FeatureMatcher<T, U>. Esse método deve extrair e retornar o valor real da propriedade inspecionada no objeto Task — portanto, retornamos actual.title.
Depois de sobrescrevermos esse método, o código-fonte completo do nosso matcher em TaskMatchers fica assim:
|
|
Pronto! Agora podemos utilizar nosso matcher customizado hasTitle() exatamente da mesma maneira que usamos os matchers padrão do Hamcrest:
|
|
Se o nosso teste falhar (por exemplo, se esperarmos "Write an example project1"), veremos a seguinte mensagem de erro formatada automaticamente pelo FeatureMatcher:
|
|
Vamos avançar e descobrir como criar um matcher customizado do Hamcrest que verifica múltiplos valores simultaneamente.
Criando um Matcher Customizado do Hamcrest para Múltiplos Valores (TypeSafeDiagnosingMatcher)
Quando precisamos criar um matcher customizado do Hamcrest que verifica se múltiplos campos combinados estão corretos (representando um estado de negócio), devemos utilizar a classe abstrata org.hamcrest.TypeSafeDiagnosingMatcher<T>. Ao estendê-la, fornecemos um parâmetro de tipo (T) que descreve o tipo do objeto inspecionado (Task).
Vamos criar um matcher customizado chamado isOpen() que verifica se um objeto Task está aberto. Em nosso domínio, uma tarefa aberta exige duas condições simultâneas: o status deve ser TaskStatus.OPEN e a resolution deve ser null. Criamos esse matcher em quatro passos:
Primeiro, adicionamos um novo método fábrica isOpen() em TaskMatchers que retorna um TypeSafeDiagnosingMatcher<Task> e não recebe parâmetros:
|
|
Segundo, modificamos nosso método fábrica para retornar uma instância anônima de TypeSafeDiagnosingMatcher<Task>():
|
|
Terceiro, sobrescrevemos o método describeTo(description: Description). Esse método é responsável por construir a primeira metade da mensagem de erro (Expected:) exibida quando a asserção falha. Ela deve descrever claramente os valores esperados: status igual a TaskStatus.OPEN e resolution igual a null:
|
|
Quarto, sobrescrevemos o método matchesSafely(item: Task, mismatchDescription: Description): Boolean. Esse método recebe dois parâmetros:
item: O objetoTaskinspecionado (já verificado contranulle com tipo garantido pelo Hamcrest).mismatchDescription: O objetoDescriptionusado para construir a segunda metade da mensagem de erro (but:) caso o matcher retornefalse, descrevendo os valores reais encontrados no objeto.
Implementamos o método matchesSafely() em dois passos:
- Descrevemos os valores reais de
item.statuseitem.resolutionemmismatchDescription. - Retornamos
trueseitem.status == TaskStatus.OPEN && item.resolution == null, efalsecaso contrário.
Depois de implementarmos o método matchesSafely(), o código-fonte completo de TaskMatchers ficará assim:
|
|
Concluímos! Agora podemos usar nosso matcher customizado isOpen() em nossos testes com uma leitura limpa e expressiva:
|
|
Se o nosso teste falhar (por exemplo, se a tarefa estiver fechada como DONE), veremos imediatamente a seguinte mensagem de erro detalhada:
|
|
Como Devemos Nomear Nossos Matchers Customizados do Hamcrest?
Devemos lembrar que o nome do nosso método fábrica é também o “nome” do matcher lido dentro do assertThat(...). As três perguntas a seguir ajudam a escolher o melhor nome para cada matcher:
- A propriedade inspecionada contém apenas um dado informativo que é exibido ou editado? Se sim, considere iniciar o nome do método fábrica com o prefixo
has(ex.:hasTitle,hasDescription). - A propriedade inspecionada registra um evento que aconteceu no passado? Se sim, considere iniciar o nome do método fábrica com o prefixo
was(ex.:wasCreatedBy,wasClosedAsDuplicateBy). - A propriedade (ou conjunto de propriedades) identifica o estado atual do objeto? Se sim, considere iniciar o nome do método fábrica com o prefixo
is(ex.:isOpen,isInProgress).
Lembre-se de que essas perguntas são diretrizes para estimular a legibilidade, não regras rígidas. Por exemplo, wasClosedAsDuplicateBy(closerId) representa um estado fechado da tarefa, mas lê-se muito melhor com was do que com is.
Quando Devemos Usar Matchers Customizados do Hamcrest?
Antes de criar qualquer matcher customizado, devemos sempre fazer esta pergunta:
Este matcher realmente tornará nosso código de teste melhor? Se sim, como?
Como matchers customizados adicionam código extra para manter na suíte de testes, só devemos criá-los quando os benefícios superarem o esforço. Vamos comparar nossos dois exemplos:
Primeiro, para verificar uma única propriedade simples (asserção centrada em dados / data-centric assertion):
|
|
Neste primeiro caso, o esforço extra não compensa. Escrever assertThat(task.title, is("title")) já é perfeitamente legível e não exige escrever 15 linhas de FeatureMatcher para cada campo de cada classe do projeto! Portanto, não devemos criar matchers customizados para simples verificações de um único campo.
Segundo, para verificar um estado de negócio composto por múltiplas propriedades:
|
|
Neste segundo caso, o esforço extra compensa enormemente por duas razões:
- A asserção
assertThat(task, isOpen())é muito mais fácil e rápida de ler e revela a intenção de negócio imediatamente. - Nosso matcher customizado remove regras de negócio de dentro dos métodos de teste. Se amanhã a definição de “tarefa aberta” passar a exigir também
closer == null, alteramos apenas o matcherisOpen()emTaskMatchers, sem precisar editar dezenas de testes espalhados pelo sistema!
Resumo
Esta aula nos ensinou quatro pontos essenciais:
- Se precisarmos criar um matcher customizado do Hamcrest para um único valor, usamos a classe
FeatureMatcher<T, U>. - Para criar um matcher customizado que valida múltiplos campos representando um estado de negócio, usamos a classe
TypeSafeDiagnosingMatcher<T>. - Devemos escolher nomes expressivos (
has...,is...,was...) para os métodos fábrica que criam nossos matchers. - Devemos criar matchers customizados especificamente para encapsular e remover regras de negócio dos nossos métodos de teste, evitando criá-los para getters simples.