Alura > Cursos de Mobile > Cursos de Flutter > Conteúdos de Flutter > Primeiras aulas do curso Testes de unidade e Widget com Mocks: boas práticas no Flutter

Testes de unidade e Widget com Mocks: boas práticas no Flutter

Criando primeiros testes - Introdução

Boas vindas! Este é o curso Testes de Unidade e Widget com mocks: boas práticas no Flutter com o instrutor Alex Felipe.

É necessário ter conhecimento sobre todo o conteúdo apresentado até o segundo curso de web API em Flutter, pois reutilizaremos o mesmo projeto feito durantes essas aulas, agora focando na garantia de funcionamento de alguns comportamentos do aplicativo a partir de testes, considerando o ambiente do Flutter.

Exploraremos a documentação e entenderemos as diversas categorias de testes vindas dos Teste de Unidade, Teste de widget e Teste de Integração, analisando quais deles serão aplicados ao longo do curso, em especial os dois primeiros.

Aprenderemos como fazer todos os tipos de teste dessas categorias, considerando alguma cobertura de nosso aplicativo; garantiremos que o Dashboard seja apresentado ao acessá-lo, que o botão "Transfer" seja clicável e apresente a tela de lista de contatos, e inclusive nos certificaremos dos fluxos, como no caso de criação de um novo contato pelo preenchimento do formulário.

Descobriremos a melhor forma de criar testes para atestar o bom funcionamento destes comportamentos, e nos depararemos com diversos desafios e surpresas ao longo do caminho, pois alguns problemas não foram percebiodos no teste visual.

Nosso objetivo é identificar exatamente como o Flutter nos oferece ferramentas para gerar essas garantias. Além disso, veremos tecnicamente como utilizar objetos simulados ou mock objects a partir do pacote Mockito. Conheceremos também um novo widget chamado InheritedWidget para podermos usar a injeção de dependências e assim consiguirmos fazer os mocks, veremos alguns problemas comuns na implementação destes tipos de testes envolvendo fluxo, e entenderemos como simular estas integrações.

Este é um curso no qual conseguiremos garantir que os comportamentos principais estejam funcionando como queremos conforme os parâmetros e ações esperados.

Então vamos lá!

Criando primeiros testes - Testes no Flutter

Considerando que nosso objetivo é testar o aplicativo, o primeiro passo é a apresentação do ambiente de testes utilizando Flutter. Acessando a documentação, temos a parte de Testing & Debugging que possui a opção Testing.

Esta página é dedicada a falar sobre testes no ambiente do Flutter, e o texto introduz que é a partir destes recursos que garantiremos a consistência de um aplicativo. Em seguida, apresenta três categorias possíveis e comuns; o primeiro é o Unit Test ou Teste de Unidade, o qual foca na regra de negócio, seja uma função, classe ou métodos. Como exemplo, poderíamos tornar um teste responsável por validar uma transferência, permitindo sua criação somente quando possuir o valor acima de zero.

Além do Teste de Unidade, temos o Widget Test ou Teste de Widget, mais relacionado ao UI e aos componentes criados. Basicamente, seu objetivo é nos tornar capazes de testar um único widget, por exemplo para saber se ele recebe o valor esperado ou se apresenta o conteúdo correto.

Por fim, temos o Integration Test ou Teste de Integração, que difere dos demais por realizar testes mais longos nos quais conseguimos validar boa parte do aplicativo, seja um pequeno fluxo ou fluxo inteiro, conhecido como end-to-end ou ponta-a-ponta.

Essas são as três grandes categorias distintas de testes presentes no Flutter. Na mesma página da documentação, temos um texto explicando que um aplicativo bem testado possui muitos testes de Unidade e Widget, considerando a cobertura de nosso código. Caso queiramos adicionar o de Integração, é recomendado aplicá-lo somente nas partes mais importantes e sensíveis do projeto para garantir o funcionamento de todo o fluxo.

Mais adiante no texto, há uma tabela indicando os trade-offs com os níveis de confiança, custo de manutenção, dependências e velocidade de execução de cada categoria. O de Unidade possui baixa confiança e manutenção, enquanto as dependências são poucas devido ao isolamento do código de teste em uma classe ou função específica, sendo executado com rapidez.

No caso do Teste de Widget, os dois primeiros critérios são maiores porque estão relacionados, já que widgets possuem maior integração, levando a um maior número de dependências mas mantendo a velocidade da execução. Isso porque esse teste utiliza a AVM do Dart, dispensando a necessidade de um emulador. Portanto, é um feedkback bastante positivo, mesmo em relação a outros ambientes.

Por fim, o de Teste de Integração é bem mais confiável, mais custoso de manter e com maior quantidade de dependências. Entretanto, sua execução é lenta.

Tabela de Trade-offs para cada tipo de teste

Considerando estes trade-offs, temos a sugestão de aplicar mais os dois primeiros testes, fazendo com que o de Integração exista, mas seja menor, justamente por sua lentidão e complexidade.

Com essas informações, focaremos em conhecer melhor o ambiente de Flutter, fazer alguns Testes de Unidade e então prestar maior atenção ao Teste de Widget, afinal este é o meio termo entre os os outros, possuindo a capacidade de realizar a validação tanto de um único widget quanto de um fluxo. Há peculiaridades a serem observadas para fazer com que ele seja rápido o suficiente para nos fornecer o maior feedback.

A seguir, começaremos a entender melhor como este ambiente de testes funcionará em nosso projeto.

Criando primeiros testes - Criando testes de unidade

Feita a introdução sobre o ambiente de testes em Flutter, conheceremos a configuração de testes que será realizada em nosso projeto.

Para isso, acessaremos o projeto raiz bytebank, dentro da pasta "test", abriremos o arquivo widget_test.dart, que possui um código preparado para o projeto inicial no momento de sua criação, incluindo o contador de exemplo que mostra como funciona a implementação de um aplicativo em Flutter. Também encontraremos um teste mostrando os passos a serem considerados para um Teste de Widget.

 void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // Build our app and trigger a frame.
    await tester.pumpWidget(BytebankApp());

    // Verify that our counter starts at 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Tap the '+' icon and trigger a frame.
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verify that our counter has incremented.
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

Se o executarmos clicando no ícone play da primeira linha de main(),a execução falhará, pois o sistema tentará verificar componentes que antes estavam presentes no projeto e agora não estão mais. Sendo assim, o teste só funcionaria se criássemos o projeto novamente, visto que o modificamos bastante.

Cientes disso, apagaremos este arquivo, já que faremos cada etapa da configuração. O diretório "test" serve para criarmos os arquivos de teste em .dart, que não possui grandes diferenças em relação aos demais, tendo inclusive o método main() que faz a execução. Deletado o arquivo, começaremos a criação do nosso próprio código de teste em um novo documento chamado transaction_teste.dart.

Neste primeiro momento, criamos um Teste de Unidade para nossa classe de transferência adicionando o método main() seguido da função test(), que pede as propriedadesdescription e body. Essa última função é disponibilizada automaticamente na criação do projeto com a importação da dependência flutter_test em pubspec.yaml, dependência essa que serve tanto para Testes de Widget quanto Testes de Unidade. Entretanto, Testes de Integração necessitam de outro pacote.

Agora precisamos definir a ação a ser executada no teste, o que é feito na propriedade description. Por exemplo, queremos que nosso teste retorne o valor de uma transferência quando ela é criada. Sendo assim, vamos inserir uma string com a mensagem "Should return the value when create a transaction".

Este teste é bastante simples, pois nosso objetivo não é discutir o que é importante testar ou não, considerando as regras de Test Driven Development (ou TDD), mas sim entender como criar nosso Teste de Unidade.

No corpo de test(), implementaremos uma função que passa os valores esperados para uma instância deTransaction() (cujo modelo deverá ser importado). Nesse caso, passaremos null como ID e contato, já que nosso único objetivo é verificar o valor, que passaremos como 200.

Devolvemos a transferência criada para uma variável e incluiremos a função expect(), que nos permite enviar o que temos atualmente - a transferência que acabamos de criar - e o que esperamos receber. Sendo assim, indicaremos o recebimento de transaction.value, cujo valor esperamos que seja 200.

void main(){
  test('Should return the value when create a transaction', () {
    final transaction = Transaction(null, 200, null);
    expect(transaction.value, 200);
  });
}

Para executarmos o teste, clicaremos novamente no botão de "Play" ou usaremos o atalho "Shift + F10". Nosso primeiro teste passará normalmente, mas é importante verificarmos se ele está realmente funcionando. Para isso, alteraremos o valor de 200 para 300 em expect() e rodaremos o código novamente.

Dessa vez, teremos um retorno de falha indicando o arquivo onde ocorreu o erro, o valor esperado e o valor recebido.

package:test_api                                   expect
package:flutter_test/src/widget_tester.dart 234:3  expect
test\transaction_test.dart 7:5                     main.<fn>

Expected: <300>
  Actual: <200.0>

Assim, conseguimos criar nosso primeiro Teste de Unidade. Criaremos então um segundo teste, dessa vez validando a transferência para que esta não receba valores menores ou iguais a zero. Simulando um TDD, adicionaremos outra chamada de test() descrita como "Should show error when create transaction with value less than zero".

A descrição do teste é livre, mas disponibilizaremos nos exercícios alguns padrões que podem ser considerados para nomear testes. O padrão utilizado aqui indica o que deve acontecer, nesse caso um erro, e a situação em que isso deve acontecer.

Após a descrição, adicionaremos no corpo um expect() indicando a criação de uma Transaction() recebendo null, 0, null. No lugar de matcher, verificaremos os erros com um throwsAssertionError.

void main(){
  test('Should return the value when create a transaction', () {
    final transaction = Transaction(null, 200, null);
    expect(transaction.value, 200);
  });
  test('Should show error when create transaction with value less than zero', () {
    expect((Transaction(null, 0, null)), throwsAssertionError);
  });
}

Ao rodarmos o teste, receberemos no retorno uma mensagem informando que, apesar de esperarmos um erro, a transferência foi criada sem problemas. Dado que agora temos um teste validando esse comportamento, alteraremos o modelo Transaction de modo que ele consiga enviar esse throwsAssertionError na situação descrita.

class Transaction {
  final double value;
  final Contact contact;
  final String id;

  Transaction(
      this.id,
      this.value,
      this.contact,
      ) : assert(value > 0);

Se executarmos com o "Shift + F10", o sistema ainda nos retornará um problema, pois quando ocorre algum problema na validação, o Flutter deixa de fazer a validação. Para corrigirmos isso, faremos uma conversão no código do segundo teste para que a execução no construtor seja feita dentro de uma função, o que é feito por meio da implementação de um callback, ou seja, fazendo que com que uma função obtenha a criação da transferência.

void main(){
  test('Should return the value when create a transaction', () {
    final transaction = Transaction(null, 200, null);
    expect(transaction.value, 200);
  });
  test('Should show error when create transaction with value less than zero', () {
    expect(() => Transaction(null, 0, null), throwsAssertionError);
  });
}

Ao executarmos novamente, nosso teste passará com sucesso. Conforme criamos uma bateria de testes, deixa de fazer sentido nós os executarmos individualmente. Colocando o cursor sobre o método main() e usando o atalho "Ctrl + Shift + F10" podemos executar todos os testes de uma só vez. Se clicarmos no botão de check da janela de execução, os testes que foram realizados serão mostrados em uma lista.

Se forçarmos um erro inserindo outro valor em expect(), o sistema indicará a execução da bateria de testes e o ponto que falhou. Assim, poderemos corrigir o código diretamente e executar o teste outra vez.

A confiança desse tipo de teste não é a melhor, afinal testamos apenas uma parte do projeto. Porém, o custo de manutenção é baixo e não temos tantas dependências para testar em nosso negócio, tornando o processo bastante rápido.

Sobre o curso Testes de unidade e Widget com Mocks: boas práticas no Flutter

O curso Testes de unidade e Widget com Mocks: boas práticas no Flutter possui 174 minutos de vídeos, em um total de 45 atividades. Gostou? Conheça nossos outros cursos de Flutter em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Flutter acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas