Introdução a Teste de Widgets no Flutter

Tempo de leitura: 4 minutes

Problema

Quando estamos desenvolvendo uma aplicação, nos deparamos com a necessidade da criação de diversos fluxos, pelos quais o usuário seguirá, consequentemente isso gera a criação inúmeros widgets.

Já o usuário, dependendo do seu status na aplicação, ou seja, dependendo dos seus atributos, passando por esses fluxos criará vários cenários.

Sabendo disso, temos que ter uma garantia nossa UI esteja funcionando durante todos esses fluxos e cenários, e que continuem ao decorrer da evolução e refatoração do sistema.

Mas como resolver isso? Testando manualmente todos os inúmeros, diversos e específicos cenários um por um, toda vez que fizermos uma alteração no código?

Testes manuais que dependendo do tamanho da aplicação demoram bastante, que necessitam que o dev tenha em mente todas as possíveis possibilidades para testá-las, além de ser cabível de falha humana e além disso, o dev deverá se comportar como um usuário, o que é quase impossível já que você sabe e conhece como usar a aplicação.

Solução

Testes de Widgets, eles testam um único widget e terá como objetivo verificar se ele agirá da forma esperada, para você testar um widget, é preciso prover todas as classes necessárias para o ciclo de vida do widget a ser testado.

Além disso, o widget testado poderá receber e reagir a eventos do usuário, conseguirá instanciar widgets filhos e simulará a construção da UI.

São testes bem simples de se entender, e se parecerem bastante com testes unitários, com os mesmos é possível testar uma UI, sem precisar de toda a aplicação funcionando, como nos testes de integração

Testes de Integração: Testam toda a aplicação, ou uma grande parte dela e tem como objetivo testar todos as partes da aplicação juntos, componentes de UI, serviços, etc…

Aqui pode-se ver um comparativo entre testes unitários, de widget e integração, envolvendo sua confiança, custo de manutenção, uso de dependências e tempo de execução.

Começando a testar widgets

O seguinte widget será testado:

import 'package:flutter/material.dart';

class TestWidget extends StatelessWidget {
  static Key buttonKey = const Key('buttonKey');

  const TestWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Test Widget'),
      ),
      body: Column(
        children: [
          ElevatedButton(
              key: buttonKey,
              onPressed: () {},
              child: Container(
                width: 100,
                height: 100,
                color: Colors.red,
              ))
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Button',
        child: const Icon(Icons.add),
      ),
    );
  }
}

1. Crie o arquivo de teste:

Todos os arquivos vão na pasta “/test” e os nomes tem que haver “_test” no final para ser  reconhecido como arquivo de teste.

->Test
  test_widget_test.dart

2. Importe a biblioteca “flutter_test”.

  • Declare o main(), onde todos os seus testes irão ser executados e declarar seu bloco de teste.
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('Test Widget', (WidgetTester tester) async {
    
    
    
  });
}

Antes de passar para os próximos passos é preciso aprender sobre o WidgetTesterpumpWidgetpumppumpAndSettle e o Finder.

WidgetTester: Responsável por toda a “simulação” do seu widget, construção, gestos, etc.

pumpWidget(): Cria o widget e fala para o WidgetTester construí-lo.

pump(): Reconstrói o widget depois de um determinado período de tempo.

pumpAndSettle(): Reconstrói o widget até o último frame/estado, essencial quando se está testando widgets que contêm algum tipo de animação.

Finder: Como o nome entrega, ele é responsável por achar os widgets.

3. Agora crie o widget e procure se tudo foi construído como o esperado.

Future<void> _createWidget(WidgetTester tester) async {
  await tester.pumpWidget(
    MaterialApp(
      title: 'Flutter Teste',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const TestWidget(),
    ),
  );
  
  await tester.pump();
}

Pode-se achar um widget de diversas formas, por exemplo: achar um texto, tipo ou uma key.

Procure o texto do título da AppBar, o FloatingActionButton pelo tipo e o ElevatedButton pela key.

testWidgets('Test Widget', (WidgetTester tester) async {
  await _createWidget(tester);

  // Buscar por Texto
  expect(find.text('Teste Widget'), findsOneWidget);
  
  // Buscar por Key do Widget
  expect(find.byKey(TestWidget.buttonKey), findsOneWidget);

  // Buscar Widget por Tipo
  expect(find.byType(FloatingActionButton), findsOneWidget);
  
});

 

Testando Gestos

Como dito nesse artigo, com o teste de widget, você pode verificar a resposta e a reação dos widgets diante da interação do usuário.

Por exemplo, use aquele velho conhecido do desenvolvedor Flutter, o contador:

import 'package:flutter/material.dart';

class TestWidget2 extends StatefulWidget {
  const TestWidget2({
    Key? key,
  }) : super(key: key);

  @override
  State<TestWidget2> createState() => _TestWidget2State();
}

class _TestWidget2State extends State<TestWidget2> {
  int counter = 0;

  void incrementCounter() {
    setState(() {
      counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Counter'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '$counter',
              style: Theme.of(context).textTheme.displaySmall,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

É preciso verificar se ao pressionar o FloatingActionButton, o texto com o contador atualiza.

Utilize o WidgetTester para gerar a ação de click com o tester.tap(), que receberá um widget encontrado pelo Finder, no caso o FloatingActionButton.

Depois disso, fale para o WidgetTester reconstruir a tela com o pump()

E por fim, procure se o texto mudou de “0” para “1”, verificando se o “0” não foi encontrado, mas o “1” sim.

// ignore_for_file: no_leading_underscores_for_local_identifiers

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:teste/src/test_widget.dart';

void main() {

  Future<void> _createWidget(WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        title: 'Flutter Teste',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const TestWidget(),
      ),
    );
    
    await tester.pump();
  }
  
  testWidgets('Test Widget', (WidgetTester tester) async {
    await _createWidget(tester);

    // Buscar por Texto
    expect(find.text('0'), findsOneWidget);

    // Receberá um widget encontrado pelo localizador
    await tester.tap(find.byType(FloatingActionButton));

    // Recria a tela
    await tester.pump();

    // Esperar que seja encontrada a string que contém o número 1 e não mais o 0
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
    
  });

  
}

 

Código fonte (link)

Finalização

Espero que com esse artigo, tenha sido entendido a razão de se escrever testes de widget e tenha dado para ter uma noção de o quão fácil é testar sua UI no Flutter.

Caso gostem desse assunto, posso escrever mais sobre testes de widget, aprofundando o assunto e mostrando como fazer testes de telas complexas, envolvendo mock de serviços, gestos complexos e outros cenários.