Mockito no Flutter: testando aplicações de forma eficiente

Mockito no Flutter: testando aplicações de forma eficiente
Ricarth Lima
Ricarth Lima

Compartilhe

Sentiu que precisa elevar a qualidade do seu código Flutter a um novo patamar com testes automatizados? Daí, percebeu que existem algumas partes desses testes que precisam ser simuladas? Se você busca criar aplicações mais robustas e livres de falhas, está no lugar certo!

Talvez você esteja dando os primeiros passos com Flutter e já ouviu falar sobre a importância de testar código, querendo aplicar essa prática desde cedo. O que é ótimo!

Ou talvez, mesmo com experiência no desenvolvimento de aplicativos Flutter, nunca tenha se aventurado nos testes e percebeu que muitas empresas valorizam essa habilidade.

Quem sabe você já tenha um projeto bem desenvolvido e precise refatorar constantemente. A cada refatoração, fica evidente que ter uma forma automatizada de testar tudo seria uma grande vantagem, certo?

Para todos os casos, chegará a hora que seu código dependerá de outros programas, processos, servidores, e esses não cabe a você testar!

É nessa hora que você simula resultados e foca no teste do seu código! E é aqui que este artigo entra para te ajudar! Nele você aprenderá:

  • O que é Mockito e como ele pode ser um aliado poderoso nos seus testes;
  • Como usar Mockito para criar teste de unidade eficientes no Flutter;
  • Configuração do ambiente de desenvolvimento com Mockito;
  • Passo a passo para criar seus primeiros testes utilizando mocks;
  • Dicas e boas práticas para tirar o máximo proveito do Mockito;
  • Como avançar para casos de uso mais complexos, simulando exceções e verificando interações.

Este é apenas o começo! Vamos explorar em profundidade como utilizar o Mockito para melhorar a qualidade dos seus testes em Flutter.

Prepare-se para dominar essa ferramenta e tornar seus projetos ainda mais confiáveis e bem estruturados. 🚀

Antes de mais nada, você sabe o que são testes?

Testes automatizados de software são uma prática essencial para garantir que o código funcione como esperado, independentemente das mudanças ou atualizações.

E com o Flutter não é diferente! Na nossa tão amada ferramenta de desenvolvimentos de aplicativos, os testes automatizados ajudam a identificar problemas precocemente, evitando comportamentos inesperados que poderiam comprometer a experiência das pessoas usuárias.

Banner promocional da Alura, com um design futurista em tons de azul, apresentando dois blocos de texto, no qual o bloco esquerdo tem os dizeres:

Tipos de testes

Existem diferentes tipos de testes que podem ser aplicados em um projeto Flutter:

  • Testes de unidade: testam pequenas partes do código, como funções ou métodos, de forma isolada.
  • Testes de Widget: validam a interface de usuário, garantindo que componentes visuais funcionem corretamente.
  • Testes de integração: verificam o comportamento de várias partes do sistema em conjunto.

O que são dublês?

Imagine que você está organizando um grande evento em que cada detalhe deve funcionar perfeitamente.

Você tem várias equipes responsáveis por diferentes tarefas: uma cuida do som, outra da iluminação, outra dos convidados, e assim por diante.

Cada uma dessas equipes é fundamental para o sucesso do evento, mas nem sempre é possível contar com todas durante os ensaios. Afinal, a equipe de som pode estar ocupada em outro evento, ou o serviço de bufê pode ainda estar em preparação.

Então, para garantir que tudo estará em harmonia no grande dia, você decide simular o evento usando substitutos. Ao invés da equipe de som real, você usa um gravador que reproduz as instruções de áudio. Para o bufê, você coloca pratos vazios nas mesas, simulando o espaço que será ocupado pela comida.

Com esses substitutos, você pode testar o fluxo do evento, identificar possíveis falhas, e ajustar detalhes, sem precisar da presença de todas as equipes reais. Esse ensaio com substitutos garante que, quando o evento real acontecer, tudo funcionará como planejado, pois cada peça foi testada e ajustada, mesmo que de maneira simulada.

Deu para sacar onde estamos chegando né?

Lembra que falamos que existirão algumas partes do nosso código onde ele se conecta com “agentes externos”? Seja um banco de dados em um servidor local, seja uma API na nuvem, seja até mesmo algum hardware físico como sensores, detectores, e outros.

Pensa comigo? Faz sentido a gente testar esse código também? Não né? Afinal, se é algo externo a nossa aplicação, muito provavelmente foi desenvolvido por outra equipe, e as pessoas desenvolvedoras de lá já cobriram suas funcionalidades com testes automatizados (ou assim esperamos! 😅).

Então, tanto na nossa aplicação real, quanto nos testes, interessa para a gente apenas o resultado de algum tipo de solicitação que façamos. Pedi uma consulta em um banco de dados? Espero uma resposta condizente. Pedi a temperatura em um sensor? Também espero uma resposta que faça sentido.

Só que, cola comigo mais uma vez: eu preciso realmente fazer uma solicitação para o tal “agente externo” enquanto estou testando? Consultar bancos de dados demora, fazer requisições para APIs geralmente tem um custo financeiro, utilizar sensores pode diminuir sua vida útil.

E é exatamente aí que, ao invés de interagir de fato com o agente externo, apenas simulamos seus comportamentos! Esse é o papel do dublê!

Tipos de dublês

  • Mocks: objetos que simulam comportamentos, permitindo verificar como uma unidade de código interage com suas dependências.
  • Stubs: fornecem dados específicos para os testes, mas não verificam interações.
  • Spies: parecidos com mocks, mas também registram informações sobre como foram usados.
  • Fakes: implementações simplificadas que funcionam de forma semelhante ao código real, mas são usadas apenas em testes.
  • Dummies: objetos passivos que apenas "preenchem espaço" nos parâmetros de um método, mas não são realmente usados no teste.

Quando usar dublês?

Dublês são particularmente úteis quando:

  • Isolamento de testes: queremos testar uma unidade de código sem depender de outros componentes ou serviços externos.
  • Complexidade externa: a interação com APIs, bancos de dados ou sistemas externos pode ser simulada de maneira controlada.
  • Condições específicas: precisamos testar cenários que seriam difíceis de reproduzir com os componentes reais, como falhas de rede ou erros específicos.

Ao utilizar dublês, conseguimos criar testes mais focados e confiáveis, garantindo que cada unidade de código funcione corretamente, mesmo em situações complexas.

Quer saber mais sobre testes no Flutter? Recomendo muito a leitura do artigo Testes no Flutter: O que é? Quando usar? Como começar a aprender? daqui da Alura!

Agora que estamos na mesma página, vamos falar do Mockito, um pacote fantástico para criação de dublês no Flutter.

O que é Mockito?

Mockito é uma biblioteca que surgiu no ecossistema Java, amplamente adotada pela sua capacidade de criar mocks.

Como comentamos brevemente mais cedo, esses mocks são objetos que imitam o comportamento de objetos reais, permitindo aos desenvolvedores testar seu código de forma isolada e controlada.

A popularidade do Mockito cresceu rapidamente no mundo Java devido à sua simplicidade e eficiência. Ele se tornou uma ferramenta indispensável para pessoas desenvolvedoras que buscavam garantir a qualidade do seu código por meio de testes automatizados. Com o tempo, a utilidade do Mockito ultrapassou as fronteiras do Java.

Quando o Flutter começou a ganhar tração no desenvolvimento de aplicativos móveis, a necessidade de uma ferramenta similar para Dart, a linguagem do Flutter, se tornou evidente.

Foi assim que o Mockito encontrou seu caminho para o Flutter. Hoje, a versão do Mockito para Dart é amplamente utilizada por pessoas desenvolvedoras Flutter para simular dependências de classes e componentes.

Isso é especialmente útil durante a execução de teste de unidade, onde a precisão e a independência dos testes são cruciais.

No Flutter essa ferramenta permite que, em ambiente de teste, você:

  • Isole a lógica de uma classe, função ou método;
  • Simule o comportamento de dependências externas, como APIs ou serviços;
  • Prepare os testes para os diversos cenários, não apenas o “caminho feliz”;
  • Registre e consulte como a interação do seu código local influenciou no código simulado.

Todo esse esforço é para garantir que o código funcione corretamente em diversos cenários, sem precisar se preocupar com a complexidade das interações reais.

Por que usar o Mockito no Flutter?

O Flutter, sendo um framework de UI, possui uma arquitetura que normalmente envolve múltiplas camadas e componentes que interagem entre si.

Como conversamos, essas interações podem incluir chamadas de rede, acesso ao armazenamento local, manipulação de estado, entre outros.

Ao escrever teste de unidade para verificar o comportamento de uma classe específica, muitas vezes é necessário simular essas interações de forma controlada, e é aqui que o Mockito se destaca.

As vantagens de usar o Mockito no Flutter incluem:

  1. Isolamento de testes: permite que as pessoas desenvolvedoras isolem a lógica da classe que está sendo testada, eliminando a necessidade de se preocupar com o comportamento de dependências externas.
  2. Redução de complexidade: facilita a criação de cenários de teste complexos, onde simular todas as dependências, manualmente, seria trabalhoso e propenso a erros.
  3. Flexibilidade: mockito oferece uma API flexível para a criação de mocks, permitindo que os desenvolvedores personalizem o comportamento dos mocks segundo as necessidades específicas dos testes.
  4. Facilidade de uso: a sintaxe e os padrões do Mockito são intuitivos, tornando-o acessível tanto para desenvolvedores iniciantes quanto para os mais experientes.

Dentre as opções de pacotes, plugins e bibliotecas no ecossistema do Flutter para criação de mocks e stubs, o Mockito, como podemos conferir em sua página do pub.dev, se destaca por ser de desenvolvimento da própria equipe dart.dev, por ter pontos de likes, pub points e popularidade altos, se compatível com a maioria das plataformas do Flutter, e por ter uma documentação completa e bem detalhada.

Tudo isso faz do Mockito uma das melhores opções na sua proposta de atuação.

Configurando o Mockito no projeto Flutter

Antes de começar a usar o Mockito em um projeto Flutter, é necessário adicioná-lo como dependência no arquivo pubspec.yaml. Abaixo está um exemplo de como incluir o Mockito no projeto:

dev_dependencies:
  flutter_test:
    sdk: flutter
  mockito: ^5.0.17
  build_runner: ^2.1.11

Após adicionar as dependências, é necessário rodar o comando flutter pub get para que as dependências sejam instaladas no projeto.

Criando seu primeiro teste com Mockito

Vamos considerar um exemplo simples onde temos uma classe UserRepository que busca dados do usuário a partir de uma API. Queremos testar a lógica de obtenção dos dados sem realmente fazer uma chamada de rede. Para isso, usaremos o Mockito para criar um mock da API.

1. Entendendo o ambiente de desenvolvimento

Ainda em ambiente de desenvolvimento (e não de testes) vamos entender quais são nossas classes e suas estruturas.

Primeiro de tudo temos o modelo User, que representa uma pessoa usuária da realidade. Ela contém um id, escrito como String, um name, também String e uma idade age, escrita como inteiro.

class User {
    final String id;
  final String name;
  final int age;
  User({required this.id, required this.name, required this.age});
}

Depois, vamos considerar a existência de uma classe responsável apenas pela comunicação com uma API.

Esse servidor que fornecesse essa API pode estar preparado para diversas requisições, não apenas as relacionadas com os dados das pessoas usuárias, e por isso essa classe ApiService estará preparada para lidar com todas essas requisições.

class ApiService {
  Future<User> getUser(String userId) async {
    // Suponha que esta função faz uma chamada de rede real usando REST com http ou dio
    return User(name: 'John Doe', age: 30);
  }
  // ...
  // Outros métodos relacionados com a comunicação com a API
  // ...
}

Por fim, a classe UserRepository: faz esse “meio de campo”, essa comunicação, entre nossa classe de requisições para API e quem quer queira (no nosso código) usar as informações da pessoa usuária, seja uma tela, um widget, outra classe, outra função e por aí vai.

class UserRepository {
  final ApiService apiService = ApiService();
  Future<User> fetchUser(String userId) async {
    return await apiService.getUser(userId);
  }
}

2. Instalando o Mockito

Legal! Ambiente de desenvolvimento em mãos, é hora de voltar para nosso objetivo inicial e testar o UserRepository certo?

Daqui já tá bem claro que nossa classe ApiService, que vai fazer a comunicação com as APIs pode ser simulada, afinal, para testar de forma independente o UserRepository, precisamos que essa classe não dependa das requisições para API que a ApiService faz!

Precisamos do Mockito!

Só que, instalar essa dependência que é apenas para o ambiente de testes, não é tão trivial quanto rodar um flutter pub add mockito ou apenas adicionar a linha mockito nas dependencies do pubspec.yaml.

Dois detalhes precisam ser levados em consideração:

Primeiro, esse é um pacote para testes, então não faz sentido que, ao criar o meu aplicativo final, para publicar em uma loja como a Google Play ou a App Store, nos arquivos da aplicação, esteja lá o Mockito.

Para resolver isso, ao invés de instalar como uma “dependência”, instalaremos como uma “dependência de desenvolvimento”, que só existirá para nós, pessoas desenvolvedoras.

Para isso você pode rodar flutter pub add dev:mockito na raiz do seu projeto, e ele será instalado como uma dependência de desenvolvimento. Alternativamente você pode adicionar mockito: ^5.4.4 (ou a versão atual disponível na página do pacote) abaixo de dev_dependencies: no pubspec.yaml.

O outro detalhe importante é que o Mockito faz uso de outro pacote, o build_runner, que precisamos instalar também.

“Mas como assim? Precisar instalar um pacote para poder usar outro. Está certo isso?”

Na verdade, sim!

É comum que pacotes externos precisam gerar códigos na sua aplicação, arquivos como .dart mesmo. Imagina se todo mundo que desenvolve um pacote tivesse que fazer isso do zero usando gravação e leitura de arquivos? Seria uma perda de tempo né?

É para isso que o próprio Dart disponibiliza esse pacote build_runner. Com ele, quem desenvolve o pacote ganha uma interface para gerar códigos necessários de forma automática, e a gente (que usa o pacote) só precisa se preocupar em chamar o build_runner na hora certa e todo o código necessário será autogerado!

Parece magia, mas é tecnologia!

Para instalar o build_runner basta rodar: flutter pub add dev:build_runner na raiz do seu projeto. Se quiser saber mais sobre essa ferramenta, recomendo a leitura da página no pub.dev.

3. Criando o nosso mock

Com o Mockito, e o build_runner instalados, vamos criar nossos mocks!

Um Mock será criado no ambiente de testes, então, já temos que ter pelo menos algo como a estrutura de pastas e arquivos a seguir construída:

test/
|-- users/
|   |-- user_repository_test.dart

No arquivo user_repository_test.dart teremos, por enquanto apenas uma main;

void main(){}

Logo acima da main adicionaremos uma anotação, a @GenerateNiceMocks, nela, passamos uma lista de MockSpec tendo como subtipo a classe que queremos simular, que no caso é ApiService.

// Importação do ApiService nessa linha
import "package:mockito/annotations.dart";
import "package:mockito/mockito.dart";
@GenerateNiceMocks([MockSpec<ApiService>])
void main(){}

Por fim, precisamos dizer ao Mockito em que arquivo queremos que ele gere nosso mock, por padrão, será um arquivo na mesma pasta do teste que segue o modelo .

import "package:mockito/annotations.dart";
import "package:mockito/mockito.dart";
import "user_repository_test.mock.dart";
@GenerateNiceMocks([MockSpec<ApiService>()])
void main(){}

Não se preocupe, essa linha dará erro, e isso é esperado, pois o arquivo ainda não existe! E não somos nós que vamos criá-lo! Será o próprio Mockito usando o build_runner que falamos! Para isso podemos rodar em um terminal na raiz do projeto:

flutter pub run build_runner build

Este comando gera a classe de mock com base na anotação @GenerateNiceMocks.

Agora sim! Se a linha import "user_repository_test.mock.dart"; deixou de dar erro, significa que nosso mock foi gerado e podemos partir para usá-lo!

4. Utilizando o mock no teste

Com o mock criado as possibilidades são praticamente infinitas! Que vermos como funciona para uma requisição de solicitação dos dados de uma pessoa usuária?

Primeiro vamos inicializar tanto o UserRepository, quanto o MockApiService (objeto que simulará o ApiService) em uma função setUp, para garantir a independência entre esse teste e possíveis futuros testes que possamos fazer usando o repositório e o mock.

//... Importações
@GenerateNiceMocks([MockSpec<ApiService>()])
void main() {
    late UserRepository userRepository;
    late MockApiService mockApiService;

    setUp(() {
      mockApiService = MockApiService();
      userRepository = UserRepository();
    });
}

Agora, ainda no setUp precisamos definir os possíveis comportamentos que o MockApiService irá simular, essa etapa chamamos de “definir o stub”. No nosso caso, apenas queremos que ele simule o seguinte caso:

“Se for solicitado a busca pela pessoa usuária com id ‘1291295’ devolverá um objeto User com o mesmo id, o nome ‘Ricarth Lima’ e idade 27.”

Para isso, vamos adicionar as seguintes linhas ainda dentro do setUp:

setUp(() {
        //...
        User user = User(id: "1291295", name: "Ricarth Lima", age: 27);
        when(mockApiService.getUser("1291295")).thenAnswer((_) async => user);
});

Note que na primeira linha apenas instanciamos o objeto de User que esperamos como resposta, já na segunda linha usamos a função when do Mockito.

“When”, do inglês, significa literalmente “quando”, ou seja, estamos preparando o Mockito para “quando” algo acontecer. No caso esse “algo” é quando alguém “chamar” o método getUser do ApiService passando o ID especificado.

Já o thenAnswer significa “e então responda”, e passamos a resposta que esperamos: uma função assíncrona que devolve o User que criamos.

Agora é só testar o UserRepository certo?

Na verdade, precisamos mudar uma pequena coisa justamente nele. Pois se, do jeito que o código está agora, fizemos algo como userRepository.fetchUser("1291295"), o código do UserRepository estará preparado para chamar ApiService e não MockApiService.

Para resolver isso, no UserRepository precisamos poder receber uma instância de ApiService ao invés de só a inicializar. Fica assim:

class UserRepository {
  late final ApiService apiService;  

  UserRepository({ApiService? apiService}){
      if (apiService != null){
          this.apiService = apiService;
      }else{
          this.apiService = ApiService();
  }

  Future<User> fetchUser(String userId) async {
    return await apiService.getUser(userId);
  }
}

Agora basta apenas, na inicialização de UserRepository no teste, passarmos o mock:

setUp(() {
      mockApiService = MockApiService();
      userRepository = UserRepository(apiService: mockApiService);    

      User user = User(id: "1291295", name: "Ricarth Lima", age: 27);
        when(mockApiService.getUser("1291295")).thenAnswer((_) async => user);
    });

Note que essa “inicialização seletiva” só funciona porque na hora de autogerar a classe MockApiService o Mockito faz ela herdar de ApiService, e podemos usar a característica de polimorfismo da programação orientada a objetos.

E, finalmente, podemos partir para nosso teste! O código final fica assim:

// Importação do ApiService nessa linha
// Importação do UserRepository nessa linha

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'user_repository.dart';
import 'user_repository_test.mocks.dart';

@GenerateNiceMocks([MockSpec<ApiService>()])
void main() {
  late UserRepository userRepository;
  late MockApiService mockApiService;

  setUp(() {
    mockApiService = MockApiService();
    userRepository = UserRepository(mockApiService);    

    User user = User(id: "1291295", name: "Ricarth Lima", age: 27);
        when(mockApiService.getUser("1291295")).thenAnswer((_) async => user);
  });

  test('deve retornar dados da pessoa usuária ao chamar fetchUser', () async {
    final User result = await userRepository.fetchUser("1291295");
    expect(result.name, 'Ricarth Lima');
    expect(result.age, 27);
  });

}

5. Recapitulando

Ufa, bastante coisa né? Vamos dar uma recapitulada neste código todo para termos certeza que você entendeu tudo direitinho!

  • @GenerateNiceMocks([MockSpec()]): essa anotação indica ao Mockito que queremos gerar uma classe mock para a ApiService.
  • setUp(): configura o ambiente de teste antes de cada teste individual, garantindo que cada teste tenha uma instância limpa do UserRepository e do MockApiService.
  • when(mockApiService.getUser()): esta função prepara o Mockito para executar algum tipo de resposta quando o que for passado dentro do when for executado em qualquer parte do código de teste que use o mock em questão.
  • thenAnswer((_) async => user): já esta função define o resultado esperado, que no caso é uma resposta assíncrona retornando um user pré-configurado.
  • expect(): Verifica se os resultados retornados pelo UserRepository correspondem ao que foi configurado no mock.

6. Executando os testes

Para rodar os testes, basta usar o comando padrão do Flutter:

flutter test

Se tudo estiver configurado corretamente, o teste deve passar, indicando que o Mockito simulou a ApiService com sucesso e que o UserRepository está funcionando conforme o esperado.

Conclusão

O Mockito é uma das ferramentas mais valiosas para quem deseja elevar a qualidade dos seus testes automatizados, especialmente no ecossistema Flutter.

Sua capacidade de simular dependências e criar cenários de teste controlados torna o desenvolvimento de aplicativos mais seguro e eficiente, permitindo que você e sua equipe foque no que realmente importa: criar uma experiência de usuário impecável.

Deixa eu aproveitar que estamos terminando nossa conversa aqui sobre o Mockito e te dar 4 dicas com as melhores práticas ao implementar mocks no seu projeto:

  1. Evite Mocks desnecessários: embora os mocks sejam úteis, é importante não abusar deles. Tente testar o máximo possível de comportamento real sem depender de mocks.
  2. Mantenha os testes simples: mocks complexos podem tornar os testes difíceis de entender e manter. Mantenha os testes focados e claros, utilizando mocks apenas quando necessário.
  3. Atualize os mocks regularmente: à medida que o código evolui, os mocks podem precisar ser atualizados para refletir as mudanças nas interfaces das classes. Certifique-se de revisar e atualizar os mocks conforme necessário.
  4. Combine mocks com outros tipos de testes: testes de unidade com mocks são uma parte importante da estratégia de testes, mas não devem ser a única. Combine-os com testes de integração e de widget para cobrir outros aspectos do aplicativo.

Ao integrar o Mockito em seu teste de unidade, você não apenas melhora a robustez do seu código, mas também otimiza seu tempo, evitando a repetição de tarefas manuais. O mundo do desenvolvimento está cada vez mais automatizado, e o Mockito é uma peça-chave nesse processo.

Interessado em dominar ainda mais o Flutter e seus testes automatizados? Confira nossos cursos:

Já teve alguma experiência com o Mockito ou outros testes automatizados no Flutter? Compartilhe conosco nas redes sociais, marcando os perfis da Alura e usando a hashtag #AprendiNaAlura.

Vejo você por aí no maravilhoso mundo do Flutter! 🚀

Ricarth Lima
Ricarth Lima

Acredito que educação e computação podem mudar o mundo para melhor, em especial, juntas. Por isso além de fazer parte do Grupo Alura, sou professor, desenvolvedor de jogos educativos e criador de conteúdo! Amo Flutter e Unity!

Veja outros artigos sobre Mobile