Criando Asserções Customizadas de Domínio com AssertJ
Ao concluirmos esta aula, nós:
- Saberemos como criar asserções customizadas com o AssertJ estendendo
AbstractAssert. - Compreenderemos quando devemos utilizar asserções customizadas em nossa suíte de testes.
[!NOTE] Esta aula assume que:
Soft Assertions Não Necessariamente Nos Ajudam a Escrever Testes Limpos
Na aula anterior, vimos como agrupar múltiplas asserções usando soft assertions. Suponha que escrevamos um teste que utiliza SoftAssertions para garantir que um objeto Task recém-criado é uma tarefa aberta (open task). O código-fonte desse teste fica assim:
|
|
Embora todas as asserções sejam avaliadas juntas graças ao SoftAssertions, esse método de teste ainda possui três problemas não resolvidos:
- Ele adiciona lógica de negócio diretamente dentro do método de teste: Isso é uma má ideia. Se alterarmos a regra de negócio que define o que é uma tarefa aberta, teremos que alterar todos os métodos de teste que verificam essa regra. Isso se torna um pesadelo de manutenção.
- Ele não é tão fácil de ler quanto testes que possuem apenas uma única asserção: Cada nova asserção adicionada torna o método de teste mais longo e difícil de ler. Para piorar, aplicações reais frequentemente possuem estados complexos que exigem diversas verificações simultâneas. Se colocarmos todas essas verificações diretamente nos métodos de teste, nossos testes parecerão enigmáticos para quem os lê pela primeira vez.
- Ele expõe detalhes técnicos de implementação: Isso desvia a atenção do leitor da essência do caso de teste (“a tarefa deve estar aberta”) para detalhes técnicos de baixo nível (“o campo
statuséOPENe o camporesolutionénull”).
Vamos descobrir como podemos resolver esses três problemas de uma só vez.
Asserções Customizadas ao Resgate
Podemos solucionar nosso problema criando uma classe de asserção customizada (custom assertion class) e movendo nossas asserções de domínio do método de teste para dentro dessa classe. Podemos criar asserções customizadas com o AssertJ em Kotlin seguindo estes cinco passos:
Passo 1: Criar a classe TaskAssert
Primeiro, criamos uma classe TaskAssert. Em Kotlin, todas as classes já são public e final por padrão — o que é excelente, pois impede que outros desenvolvedores estendam nossa classe de asserção indevidamente.
O código-fonte inicial da nossa classe de asserção customizada fica assim:
|
|
[!TIP] Convenção de Nomenclatura para Classes de Asserção Ao nomear classes de asserção customizadas, uma ótima prática é acrescentar o sufixo
Assertao nome do tipo do objeto verificado. Como nossa classe é usada para escrever asserções sobre objetosTask, seu nome deve serTaskAssert.Além disso, se a classe de asserção for usada apenas dentro do mesmo módulo ou pacote, podemos restringir sua visibilidade com
internal class TaskAssert.
Passo 2: Estender a classe AbstractAssert
Segundo, modificamos nossa classe de asserção customizada para estender a classe genérica AbstractAssert<S : AbstractAssert<S, A>, A> do AssertJ. Ao estendermos AbstractAssert, devemos fornecer dois parâmetros de tipo:
- O próprio tipo da nossa classe de asserção customizada (
TaskAssert). - O tipo do objeto que será verificado (
Task).
Após estendermos AbstractAssert, o código da classe TaskAssert fica assim:
|
|
Passo 3: Adicionar o construtor privado
Terceiro, adicionamos um construtor private à classe TaskAssert seguindo estes passos:
- Garantimos que o construtor receba o objeto
Taskreal (actual: Task?) como argumento. - Invocamos o construtor da superclasse
AbstractAssert, passando dois argumentos:- O objeto real (
actual). - A referência
Classque representa nossa classe de asserção customizada (TaskAssert::class.java).
- O objeto real (
Depois de adicionarmos esse construtor à classe TaskAssert, o código-fonte fica assim:
|
|
Passo 4: Adicionar o método fábrica (assertThatTask)
Quarto, adicionamos um método fábrica estático (em um companion object ou como função top-level em Kotlin) que retorna um objeto TaskAssert e recebe o objeto Task verificado como parâmetro.
Após adicionarmos esse método fábrica à classe TaskAssert, o código-fonte fica assim:
|
|
[!NOTE] Por que
assertThatTask()em vez deassertThat()? Ao nomear métodos fábrica de asserções customizadas, acrescentar o tipo do objeto verificado ao prefixoassertThat(formandoassertThatTask()) evita conflitos de importação estática na IDE quando o mesmo teste utiliza tantoAssertions.assertThat()padrão do AssertJ quanto a nossa asserção customizada.
Passo 5: Implementar o método de asserção isOpen()
Quinto, adicionamos o método de asserção customizado à classe TaskAssert seguindo estes passos:
- Adicionamos um método público
isOpen(): TaskAssertà classeTaskAssert. Esse método não recebe parâmetros e retorna uma referência ao próprio objetoTaskAssert. - Verificamos que o objeto
actualnão é nulo (isNotNull) e que ele representa uma tarefa aberta utilizando a classeSoftAssertions(notando que a propriedade herdadaactualdoAbstractAssertguarda a referência para oTaskavaliado). - Retornamos
thisao final do método, permitindo encadear múltiplas asserções customizadas de forma fluente.
Depois de adicionarmos o método isOpen() à nossa classe de asserção customizada, o código completo da TaskAssert fica assim:
|
|
Pronto! Após refatorarmos nosso método de teste para utilizar nossa nova asserção customizada assertThatTask(task).isOpen(), o código-fonte da nossa classe de teste fica extremamente limpo:
|
|
Vamos avançar e descobrir como devemos nomear nossas asserções customizadas.
Como Devemos Nomear Nossas Asserções Customizadas?
[!NOTE] Esta seção segue os mesmos princípios que vimos para nomear nossos Matchers Hamcrest customizados.
Devemos lembrar que o nome do nosso método na classe TaskAssert é também o “nome” da nossa asserção na DSL fluente lida no teste. Dar bons nomes a métodos de asserção é essencial, e as seguintes perguntas nos ajudam a escolher o melhor nome:
- A propriedade verificada contém informações que são simplesmente exibidas e editadas? Se sim, considere nomear o método de asserção adicionando o prefixo
hasao nome da propriedade (ex.:hasTitle(expectedTitle)). - A propriedade verificada contém informações sobre um evento que aconteceu no passado? Se sim, considere iniciar o nome do método de asserção com o prefixo
was(ex.:wasCreatedBy(creatorId)). - A propriedade (ou conjunto de propriedades) verificada identifica o estado atual do objeto? Se sim, considere iniciar o nome do método de asserção com o prefixo
is(ex.:isOpen(),isInProgress()).
É importante lembrar que essas perguntas são diretrizes e não leis rígidas. Seu único propósito é nos ajudar a encontrar o nome mais legível possível, e às vezes devemos abrir exceções quando outro nome soar mais natural no domínio.
Por exemplo, se quisermos criar uma asserção customizada que garante que uma tarefa foi fechada como duplicada (DUPLICATE) por uma pessoa X, um excelente nome é wasClosedWithResolutionDuplicateBy(userId) — mesmo que ela verifique o estado da tarefa.
Vamos avançar e descobrir quando devemos utilizar asserções customizadas.
Quando Devemos Usar Asserções Customizadas?
Antes de criarmos uma asserção customizada, devemos sempre fazer esta pergunta:
Esta asserção tornará o nosso código de teste melhor? Se sim, como?
Como asserções customizadas adicionam código extra para manter em nossa suíte de testes, só devemos utilizá-las quando seus benefícios superarem seus custos. Vamos demonstrar esse princípio comparando dois cenários práticos.
Cenário 1: Asserções Centradas em Dados Simples (Data-Centric)
Se precisarmos escrever uma asserção que apenas garante que o título (title) de uma tarefa está correto, podemos comparar as duas abordagens:
|
|
Neste caso, o esforço extra de criar um método customizado geralmente não compensa. A asserção customizada é ligeiramente mais fluente, mas a asserção padrão do AssertJ já é perfeitamente clara e legível.
Além disso, há outro motivo pelo qual devemos evitar asserções customizadas para simples propriedades de dados:
Onde paramos? Deveríamos escrever um método de asserção customizado para cada campo de cada classe do sistema? Criar e manter métodos customizados leva tempo, e não devemos desperdiçar esforço quando o retorno sobre o investimento (ROI) é quase nulo.
Portanto, em geral, não devemos criar asserções customizadas para verificações puramente centradas em dados simples — a menos que queiramos destacar um conceito composto importante do domínio (como um endereço completo) ou construir uma linguagem específica de domínio (DSL) rica para encadeamento fluente.
Cenário 2: Asserções que Encapsulam Regras de Negócio e Estado
Agora, se precisarmos garantir que uma tarefa recém-criada é uma tarefa aberta, compare as duas opções:
|
|
Ao compararmos os dois blocos, fica imediatamente evidente que aqui o esforço extra vale muito a pena por três razões:
- A asserção que utiliza
assertThatTask(task).isOpen()é drasticamente mais fácil e rápida de ler do que o bloco manual de asserções. - Nossa asserção customizada remove regras de negócio de dentro do método de teste, centralizando-as em um único lugar reutilizável. Se a definição de “tarefa aberta” mudar no futuro, precisaremos atualizar apenas o método
isOpen()na classeTaskAssert, em vez de caçar dezenas de métodos de teste espalhados pelo projeto. - Nossa asserção customizada oculta detalhes técnicos de implementação, mantendo o foco do leitor na intenção de negócio do caso de teste.
Resumo
Nesta aula, aprendemos quatro pontos essenciais:
- Se quisermos criar asserções customizadas com o AssertJ, devemos estender a classe base
AbstractAssert. - Devemos escolher nomes expressivos para nossos métodos de asserção (
has...,was...,is...), pois esses nomes formam a linguagem fluente lida nos testes. - Só devemos criar asserções customizadas centradas em dados simples se quisermos destacar propriedades compostas específicas ou construir uma DSL fluente de domínio.
- Devemos sempre utilizar asserções customizadas para remover regras de negócio e detalhes técnicos de implementação de dentro dos nossos métodos de teste.