Em testes de software, frequentemente nos encontramos na situação de querer comparar o valor atual com o valor esperado.

É nesse momento que os matchers aparecem. Um matcher é basicamente um objeto que segue uma interface específica para realizar essa comparação.

Eles só possuem uma finalidade, 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 verificar se a variável “é igual a 5”, “é maior que 2” ou “é um número inteiro”.

Pô, Caique, puta trabalho só para comparar dois valores. Isso ai vai atrasar minha entrega

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

Agora que sabemos o que é um matcher, vamos descobrir 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 mismatchDescriptio,
    Map matchState,
    bool verbose,
  ) => mismatchDescription;
}

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

  1. matches: É o método responsável por conter a lógica de um match. Ele recebe um item para ser comparado e um mapState. O seu retorno é um boleano que indica se o item satisfaz todas as condições impostas pelo Matcher.
  2. describe: Responsável por gerar a descrição do que o Matcher está realizando o match.
  3. describeMismatch: Responsável por dar mais contexto do mismatch que ocorreu. Esse método é invocado quando o método matches retorna falso.

Entendo um pouco mais sobre a função matches e describeMismatch

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

Quando utilizar o describeMismatch

  1. Validações complexas: Se o Matcher é responsável por realizar multiplas validações é interessante indicar a razão do teste ter falhado.
  2. Dado adicionais: As vezes o código sendo validado depende de algum dado computado. Podemos indicar isso através do mapState. Visualizando o processo

Agora que sabemos o que cada função faz, seria interessante entendermos 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

Depois de tudo isso, bora lá para a nossa pratica?

Colocando o Matcher para trabalhar

Finalmente a teoria acabou e chegou a hora da pratica. Bora entender como criar um Matcher para facilitar nossa vida?

O cenário

Imagine que voce esta desenvolvendo uma tela em Flutter e precisa testar um widget que recebe um e-mail como entrada. Para facilitar a nossa vida poderíamos

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 de teste, vimos o funcionamento de um Matcher. A partir dai, reconhecemos ele como uma ferramenta que pode transformar a maneira como escrevemos e lemos nossos testes.

No exemplo pratico, demonstramos como podemos reduzir a duplicidade de código em testes através do emprego de Matchers, otimizando o processo e tornando tudo mais claro.

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

Até a próxima pessoal!