Article image

DM

Daniel Melonari19/06/2024 08:58
Compartilhe

Trabalhando com Mocks em Dart: Um Guia Completo

  • #Dart

Introdução

Mocking é uma técnica essencial em testes de software que permite simular objetos ou dependências externas. Em Dart, criar mocks pode ajudar a testar interações entre classes sem a necessidade de se conectar a serviços reais ou manipular dados reais. Neste artigo, vamos explorar como criar e utilizar mocks em Dart, fornecendo uma abordagem robusta e profissional.

O que é Mocking?

Mocking envolve criar objetos simulados que imitam o comportamento de objetos reais. Esses mocks podem retornar dados simulados, registrar chamadas de métodos e verificar interações. Ao entender como criar mocks, você pode melhorar significativamente a qualidade e a confiabilidade dos seus testes.

Por Que Utilizar Mocks?

Mocks são úteis para:

  • Isolar o código que está sendo testado: Garantir que os testes não dependem de componentes externos.
  • Simular diferentes cenários: Testar como o código se comporta sob diferentes condições.
  • Verificar interações: Assegurar que métodos específicos são chamados com os parâmetros corretos.

Criando uma Classe de Mock do Zero

Vamos criar uma classe de mock que pode interceptar chamadas de métodos, definir respostas simuladas e registrar interações. Utilizaremos noSuchMethodInvocation e Symbol para alcançar isso.

noSuchMethod

O método noSuchMethod é um recurso do Dart que permite interceptar chamadas para métodos que não estão definidos na classe. Isso é extremamente útil para criar mocks, pois podemos capturar essas chamadas e fornecer respostas simuladas.

Invocation

A classe Invocation representa uma chamada de método ou acesso a uma propriedade. Ela contém informações como o nome do método, argumentos posicionais e nomeados.

Symbol

Symbol é usado para representar o nome de um método ou propriedade de forma segura e única. Isso nos ajuda a identificar qual método foi chamado.

Implementação da Classe de Mock

import 'dart:async';
import 'package:collection/collection.dart';

/// Classe base para criar objetos mock. Esta classe fornece
/// funcionalidades para definir implementações mock de métodos,
/// registrar chamadas a esses métodos e verificar as interações.
class Mock {
final _answers = <Symbol, Function(Invocation)>{};
final _calls = <Symbol, List<Invocation>>{};

/// Intercepta chamadas a métodos indefinidos e fornece
/// implementações mock se definidas. Se nenhuma implementação
/// mock for encontrada, chama o método noSuchMethod da superclasse.
@override
dynamic noSuchMethod(Invocation invocation) {
  _registerCall(invocation);
  final answer = _answers[invocation.memberName];
  if (answer != null) {
    return answer(invocation);
  }
  return super.noSuchMethod(invocation);
}

/// Define uma implementação mock para um dado método.
///
/// [realFunction] é o símbolo do método real.
/// [function] é a função que define a implementação mock.
void when(Symbol realFunction, Function(Invocation) function) {
  _answers[realFunction] = function;
}

/// Verifica as chamadas feitas a um método específico.
///
/// [realFunction] é o símbolo do método real.
/// [verification] é uma função que recebe uma lista de invocações e realiza a verificação.
void verify(Symbol realFunction, void Function(List<Invocation>) verification) {
  final calls = _calls[realFunction] ?? [];
  verification(calls);
}

/// Registra uma chamada feita a um método mockado.
///
/// [invocation] é a invocação do método.
void _registerCall(Invocation invocation) {
  final calls = _calls.putIfAbsent(invocation.memberName, () => []);
  calls.add(invocation);
}

/// Limpa todas as interações registradas.
void clearInteractions() {
  _calls.clear();
}

/// Limpa todas as respostas definidas.
void clearAnswers() {
  _answers.clear();
}

/// Verifica se um método foi chamado um número específico de vezes.
///
/// [realFunction] é o símbolo do método real.
/// [times] é o número de vezes esperado que o método tenha sido chamado.
void verifyCalledTimes(Symbol realFunction, int times) {
  final calls = _calls[realFunction] ?? [];
  if (calls.length != times) {
    throw AssertionError('Esperado que $realFunction fosse chamado $times vezes, mas foi chamado ${calls.length} vezes.');
  }
}

/// Verifica se um método foi chamado com argumentos específicos.
///
/// [realFunction] é o símbolo do método real.
/// [positionalArgs] são os argumentos posicionais esperados.
/// [namedArgs] são os argumentos nomeados esperados (opcional).
void verifyCalledWith(Symbol realFunction, List<dynamic> positionalArgs, [Map<Symbol, dynamic>? namedArgs]) {
  final calls = _calls[realFunction] ?? [];
  final matchedCalls = calls.where((call) {
    final positionalMatch = const ListEquality().equals(call.positionalArguments, positionalArgs);
    final namedMatch = const MapEquality().equals(call.namedArguments, namedArgs ?? {});
    return positionalMatch && namedMatch;
  }).toList();

  if (matchedCalls.isEmpty) {
    throw AssertionError('Esperado que $realFunction fosse chamado com $positionalArgs e $namedArgs, mas não foi.');
  }
}
}

Explicação dos Métodos:

  • noSuchMethod: Este método intercepta chamadas a métodos que não estão definidos na classe. Ele registra a chamada e tenta encontrar uma implementação mock. Se não encontrar, chama o noSuchMethod da superclasse.
  • when: Define uma implementação mock para um método específico. Recebe o símbolo do método e a função que define a implementação mock.
  • verify: Verifica as chamadas feitas a um método específico. Recebe o símbolo do método e uma função de verificação que processa a lista de invocações.
  • _registerCall: Registra uma chamada feita a um método mockado. Adiciona a invocação à lista de chamadas.
  • clearInteractions: Limpa todas as interações registradas, útil para resetar o mock entre testes.
  • clearAnswers: Limpa todas as respostas definidas, útil para redefinir implementações mock entre testes.
  • verifyCalledTimes: Verifica se um método foi chamado um número específico de vezes. Lança um erro se o número de chamadas não corresponder ao esperado.
  • verifyCalledWith: Verifica se um método foi chamado com argumentos específicos. Lança um erro se não encontrar uma correspondência.

Definindo a Interface do Repositório

Vamos definir uma interface de repositório que nossa classe de mock irá implementar. Isso nos permitirá simular interações com um repositório real.

/// Interface que define o contrato do repositório.
abstract class IRepository {
Future<String> fetchData(String params);
}

Criando o Caso de Uso

Nosso caso de uso GetData utiliza o repositório para buscar dados. Esta classe será usada para testar nosso mock.

/// Caso de uso que utiliza um repositório para buscar dados.
class GetData {
final IRepository repository;

GetData(this.repository);

/// Chama o método fetchData do repositório com os parâmetros fornecidos.
Future<String> call(String params) async {
  return await repository.fetchData(params);
}
}

Implementando o Mock do Repositório

Agora, implementamos o mock do repositório, estendendo a classe Mock e implementando a interface IRepository.

/// Mock do repositório que implementa a interface IRepository.
class RepositoryMock extends Mock implements IRepository {}

Utilizando o Mock em Testes

Finalmente, vamos utilizar nosso mock em um teste. Definimos a implementação mock para o método fetchData e verificamos as interações usando o pacote de testes do Dart.

Adicione o pacote test ao seu pubspec.yaml:

dev_dependencies:
test: ^1.16.0

Escrevendo Testes

import 'package:test/test.dart';

void main() {
test('Deve retornar "dados" ao chamar fetchData', () async {
  final repositoryMock = RepositoryMock();

  // Define a implementação mock para o método fetchData.
  repositoryMock.when(const Symbol('fetchData'), (invocation) async {
    return 'dados';
  });

  final useCase = GetData(repositoryMock);

  // Chama o caso de uso com o parâmetro 'nome do produto'.
  final result = await useCase('nome do produto');

  expect(result, 'dados');

  // Verifica se o método fetchData foi chamado uma vez com o parâmetro 'nome do produto'.
  repositoryMock.verify(const Symbol('fetchData'), (calls) {
    expect(calls.length, 1);
    expect(calls.first.positionalArguments[0], 'nome do produto');
  });

  // Verifica se o método fetchData foi chamado uma vez.
  expect(() => repositoryMock.verifyCalledTimes(const Symbol('fetchData'), 1), returnsNormally);

  // Verifica se o método fetchData foi chamado com o parâmetro 'nome do produto'.
  expect(() => repositoryMock.verifyCalledWith(const Symbol('fetchData'), ['nome do produto']), returnsNormally);
});
}

Explicação dos Testes:

Definição da Implementação Mock: Usamos o método when para definir a implementação mock do método fetchData. Sempre que fetchData for chamado com qualquer parâmetro, ele retornará a string 'dados'.

Chamando o Caso de Uso: Criamos uma instância de GetData passando o mock do repositório e chamamos o método call com o parâmetro 'nome do produto'.

Verificações:

  • Verificação de Chamadas: Usamos o método verify para garantir que o método fetchData foi chamado uma vez com o parâmetro 'nome do produto'.
  • Verificação de Número de Chamadas: Usamos verifyCalledTimes para garantir que o método fetchData foi chamado exatamente uma vez.
  • Verificação de Argumentos: Usamos verifyCalledWith para garantir que fetchData foi chamado com o argumento 'nome do produto'.

Conclusão

Criar mocks em Dart pode ser uma tarefa simples, mas é importante adotar uma abordagem robusta para garantir que seus testes sejam eficazes e fáceis de manter. Neste artigo, exploramos como criar uma classe de mock completa, que permite interceptar chamadas de métodos, definir respostas mock, registrar e verificar interações. Com essas ferramentas, você pode testar suas classes de maneira mais eficiente e garantir que seu código está funcionando conforme esperado.

Espero que este guia tenha sido útil. Se você tiver alguma dúvida ou sugestão, sinta-se à vontade para deixar um comentário!

Compartilhe
Comentários (0)