Em testes de software, frequentemente nos deparamos com a necessidade de comparar o valor atual com o valor esperado. É nesse momento que os matchers se tornam essenciais. Um matcher é, basicamente, um objeto que segue uma interface específica para realizar essa comparação.

O principal objetivo dos matchers é criar testes mais descritivos, legíveis e robustos. Isso nos permite expressar as condições que esperamos de uma forma muito mais clara e fluída.

Por exemplo, em vez de usar um expect para verificar se uma variável é igual a 5, poderíamos utilizar um matcher para confirmar se a variável “é igual a 5”, “é maior que 2” ou “é um número inteiro”.

Pô, Caique, isso dá muito trabalho para comparar apenas dois valores. Isso vai atrasar minha entrega.

Calma lá! A gente só gasta horas em uma tarefa quando não compreendemos como realizá-la. Para evitar que os testes se tornem um fardo, vamos nos aprofundar um pouco mais nesse tipo de objeto.

Agora que sabemos o que é um matcher, vamos explorar como eles funcionam. Para isso, vamos dar uma olhada no código dessa classe:

/// The base class for all matchers.
///
/// [matches] and [describe] must be implemented by subclasses.
///
/// Subclasses can override [describeMismatch] if a more specific description is
/// required when the matcher fails.
abstract class Matcher {
  const Matcher();
 
  /// Does the matching of the actual vs expected values.
  ///
  /// [item] is the actual value. [matchState] can be supplied
  /// and may be used to add details about the mismatch that are too
  /// costly to determine in [describeMismatch].
  bool matches(dynamic item, Map matchState);
 
  /// Builds a textual description of the matcher.
  Description describe(Description description);
 
  /// Builds a textual description of a specific mismatch.
  ///
  /// [item] is the value that was tested by [matches]; [matchState] is
  /// the [Map] that was passed to and supplemented by [matches]
  /// with additional information about the mismatch, and [mismatchDescription]
  /// is the [Description] that is being built to describe the mismatch.
  ///
  /// A few matchers make use of the [verbose] flag to provide detailed
  /// information that is not typically included but can be of help in
  /// diagnosing failures, such as stack traces.
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map matchState,
    bool verbose,
  ) => mismatchDescription;
}

Essa classe descreve um contrato que deve ser seguido pelas suas especializações. Os principais métodos são:

  • matches: Este método contém a lógica de um match. Ele recebe um item para ser comparado e um matchState. O retorno é um booleano que indica se o item satisfaz todas as condições impostas pelo Matcher.

  • describe: Responsável por gerar a descrição do que o Matcher está realizando o match.

  • describeMismatch: Oferece mais contexto sobre o mismatch que ocorreu. Esse método é invocado quando o método matches retorna falso.

Compreendendo as Funções matches e describeMismatch

Um dos parâmetros que a função matches recebe é um Map, que tem como finalidade enriquecer as informações que o matches irá passar para o describeMismatch.

Quando Utilizar o describeMismatch

  • Validações complexas: Se o Matcher é responsável por realizar múltiplas validações, é interessante indicar a razão pela qual o teste falhou.

  • Dados adicionais: Às vezes, o código sendo validado depende de algum dado computado. Podemos indicar isso através do matchState.

Visualizando o Processo

Agora que sabemos o que cada função faz, vamos entender como elas interagem entre si. Para isso, iremos desenhar um diagrama de sequência com Mermaid:

sequenceDiagram
    participant Teste
    participant FrameworkDeTeste
    participant MatcherPersonalizado
    participant Descrição

    Teste ->> FrameworkDeTeste: expect(valorAtual, matcher)
    FrameworkDeTeste ->> MatcherPersonalizado: matches(item, estadoDaCorrespondência)

    alt correspondência é false
        MatcherPersonalizado ->> Descrição: describe(descrição)
        Descrição -->> MatcherPersonalizado: textoDaDescrição
        MatcherPersonalizado ->> Descrição: add(textoDaDescrição)
        Descrição -->> FrameworkDeTeste: textoDoErro
        FrameworkDeTeste -->> Teste: Lança erro de asserção com textoDoErro
    else correspondência é true
        FrameworkDeTeste -->> Teste: Teste passa
    end

Colocando o Matcher para Trabalhar

Finalmente, a teoria acabou e chegou a hora da prática. Vamos entender como criar um Matcher para facilitar nossa vida?

O Cenário

Imagine que você está desenvolvendo uma tela em Flutter e precisa testar um widget que recebe um e-mail como entrada. Para facilitar nossa vida, poderíamos criar um matcher da seguinte forma:

class IsValidEmail extends Matcher {
  @override
  bool matches(
    dynamic item,
    Map matchState,
  ) {
    if (item is! String) {
      matchState['reason'] = 'Not a string';
      return false;
    }
 
    if (!item.contains('@')) {
      matchState['reason'] = 'Missing @ symbol';
      return false;
    }
 
    List<String> parts = item.split('@');
    if (parts.length != 2) {
      matchState['reason'] = 'Multiple @ symbols';
      return false;
    }
 
    if (parts[0].isEmpty || parts[1].isEmpty) {
      matchState['reason'] = 'Local or domain part is empty';
      return false;
    }
 
    if (!parts[1].contains('.')) {
      matchState['reason'] = 'Missing domain extension';
      return false;
    }
 
    return true;
  }
 
  @override
  Description describe(Description description) {
    return description.add('is a valid email');
  }
 
  @override
  Description describeMismatch(
    item,
    Description mismatchDescription,
    bool verbose,
    Map matchState,
  ) {
    return mismatchDescription.add('Failed because ${matchState['reason']}');
  }
}

E Agora?

No nosso segundo artigo sobre testes, vimos o funcionamento de um Matcher. A partir daí, reconhecemos o matcher como uma ferramenta que pode transformar a maneira como escrevemos e lemos nossos testes.

No exemplo prático, demonstramos como podemos reduzir a duplicidade de código em testes através do uso de matchers, otimizando o processo e tornando tudo mais claro.

Espero que este artigo tenha ajudado no entendimento dos matchers. Nos próximos artigos, vamos entender como funciona o processo de um teste de widget e como os matchers podem nos ajudar nesse contexto.

Até a próxima, pessoal!