DEV Community

Walter Merscher
Walter Merscher

Posted on

3 1 1 1

Começando com testes no Flutter

O básico

O melhor lugar para começarmos é pelo básico, e um dos melhores exemplos para quem começou a aprender a desenvolver em Flutter é o projeto inicial, o famoso CounterApp, que é um projeto simples contendo apenas um botão de “+” e um contador que incrementamos ao pressionar o botão.

Assim que criamos um projeto temos a seguinte estrutura de código no nosso arquivo main.dart:

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Então podemos prosseguir e analisar como podemos montar nossos testes para essa tela, e o projeto já nos dá uma breve introdução aos testes de widget por meio de arquivo test/widget_test.dart:

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

    // 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);
  });
}
Enter fullscreen mode Exit fullscreen mode

Esse é um teste bem simples, ele tem o único objetivo de verificar se ao pressionar o botão "+” o contador na tela é incrementado, mas como ele faz isso é o que vamos ver.

Como executar os testes

Antes de tudo para executarmos todos os testes que se encontram na pasta /test do nosso projeto basta executarmos o seguinte comando no terminal na pasta do projeto:

flutter test
Enter fullscreen mode Exit fullscreen mode

Para executarmos apenas um arquivo de teste específico basta adicionar o caminho e nome do arquivo ao final:

flutter test test/widget_test.dart
Enter fullscreen mode Exit fullscreen mode

main()

void main() {
..
}
Enter fullscreen mode Exit fullscreen mode

A primeira coisa que conseguimos notar é que da mesma forma que nossa aplicação, nosso teste também é iniciado dentro de uma função main() , mas diferente da aplicação ele consegue identificar que é um teste simplesmente pelo nome do arquivo, então os arquivos que possuem test ao final do nome são considerados arquivos de teste, por isso ele não vai exigir um aparelho ou emulador para fazer a execução.

Da mesma forma se não nomearmos nosso arquivo de teste da maneira correta ele vai tentar executar nossos testes como uma aplicação, e claro esse não é o comportamento que esperamos.

testWidgets()

testWidgets('Counter increments smoke test', (WidgetTester tester) async {
...
});
Enter fullscreen mode Exit fullscreen mode

Para iniciar um teste precisamos de um bloco de código com a única responsabilidade de executar nosso cenário especifico, e essa é a função do testWidgets() . E para que ela funcione precisamos de dois parâmetros:

description : a descrição do teste, por meio dele podemos esclarecer o que nosso teste faz.

callback : recebe o bloco de teste que será executado, sendo possível executar código assíncronos utilizando async/await .

Essa função foi criada para testar widgets, mas se no caso você deseja fazer apenas um teste unitário você pode usar a função test() que tem a mesma assinatura mas não tem acesso ao WidgetTester .

test('Counter increments smoke test', () {
...
});
Enter fullscreen mode Exit fullscreen mode

tester.pumpWidget()

await tester.pumpWidget(const MyApp());
Enter fullscreen mode Exit fullscreen mode

Essa é a função responsável por renderizar a UI do Widget recebido para o teste, ela vai fazer a chamada da função runApp() da mesma forma que é feita no arquivo main.dart para iniciar nossa aplicação. Por isso é necessário que o widget informado seja um MaterialApp.

Nesse cenário o teste vai funcionar normalmente, mas se precisamos testar outro widget que não possui o MaterialApp no início da árvore precisamos adicioná-lo.

await tester.pumpWidget(MaterialApp(home: const MySecondPage()));
Enter fullscreen mode Exit fullscreen mode

expect()

expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
Enter fullscreen mode Exit fullscreen mode

Essa função é o ponto vital do nosso teste, ela que é responsável por dizer se nosso teste foi um sucesso ou uma falha. Se o valor esperado for igual ao valor recebido o teste é um sucesso. Para isso recebemos dois parâmetros, o primeiro é o valor ou variável que queremos verificar e o segundo é a condição que desejamos atender.

Se estiver comparando dois objetos com instâncias diferentes é necessário fazer o override da função == e do campo hasCode do objeto para considerar os valores dos campos ao invés da instância.

Dessa forma podemos ler a primeira chamada do expect da seguinte forma:

Eu espero que esse campo (find.text('0')) possua esse valor (findsOneWidget)

Mas o que significam esses campos?

A variável find é um objeto próprio da biblioteca de teste do flutter que nos permite ter acesso a várias funções que podemos usar para encontrarmos nossos widgets na tela, como por exemplo:

ancestor(...)
byElementPredicate(...)
byElementType(...)
byIcon(...)
byKey(...)
byType(...)
byWidget(...)

Em meio a quantidade de métodos podemos ficar perdidos sobre qual usar, mas o ideal aqui é sempre tentar encontrar o widget que queremos da forma mais simples possível, e é isso que é feito no teste de exemplo.

Ao declarar find.text('0') ele deseja encontrar qualquer widget na tela que possua um texto com o valor ‘0’, esse é o valor inicial do nosso contador ao iniciar a tela.

O campo findsOneWidget é o nosso matcher, ele é responsável por responder se existe pelo menos 1 widget com o valor que declaramos.

E podemos usar outras variações, como por exemplo:

  • findsNothing - espera que o finder não encontre nada
  • findsWidgets - espera que o finder encontre um ou mais widgets
  • findsNWidgets - espera que o finder encontre uma quantidade específica de widgets que nós informamos
  • findsAtLeastNWidgets - espera que o finder encontre pelo menos uma quantidade mínima de widgets que nós informamos

Agora ao voltarmos ao exemplo inicial podemos entender o seguinte:

expect(find.text('0'), findsOneWidget);
Enter fullscreen mode Exit fullscreen mode

Espero que seja encontrado apenas um widget com o texto '0'

expect(find.text('1'), findsNothing);
Enter fullscreen mode Exit fullscreen mode

Espero que não seja encontrado nenhum um widget com o texto '1'

tester.tap()

await tester.tap(find.byIcon(Icons.add));
Enter fullscreen mode Exit fullscreen mode

A função tester.tap() tem um nome bem sugestivo, em casos onde queremos acionar o gesto de pressionar de algum widget é ele que faz isso.

Nesse teste por exemplo ele primeiro vai usar o finder para encontrar algum widget que usa o ícone Icons.add , e ao encontrar, ele vai acionar o tap ou pressioná-lo.

Após isso esperamos então que a nossa tela seja atualizada modificando o valor de "0” pra "1”, e não queremos encontrar o texto "0” e sim "1”.

expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
Enter fullscreen mode Exit fullscreen mode

Mas se executarmos nosso teste assim teremos um erro porque nossa função tap realizou uma mudança de estado na tela mas nosso widget não foi atualizado.

Vamos ver como resolver isso com a função tester.pump().

tester.pump()

await tester.pump();
Enter fullscreen mode Exit fullscreen mode

Quando executamos um teste de widget ele é renderizado e os testes são executados em cima do frame inicial da tela, mas quando temos uma mudança de estado no nosso widget nosso teste precisa saber disso, e essa função faz isso, diz ao teste que o estado do widget precisa ser atualizado para o próximo frame.

Nesse exemplo, após pressionarmos o botão de "+” esperamos que o frame da tela seja atualizado para que o texto "0” seja substituído por "1”, então precisamos chamar essa função para que ele entenda que precisa fazer isso.

await tester.pump();
Enter fullscreen mode Exit fullscreen mode

Também podemos passar para função uma duração se quisermos que a atualização seja feita após algum período.

await tester.pump(Duration(seconds: 5));
Enter fullscreen mode Exit fullscreen mode

Ou podemos usar o pumpAndSettle() que vai esperar pelo próximo frame para continuar o teste.

await tester.pumpAndSettle();
Enter fullscreen mode Exit fullscreen mode

Agora se executarmos o teste novamente ele vai ser concluído com sucesso.

void main() {
  testWidgets('Deve verificar se o contador está sendo incrementado', (WidgetTester tester) async {
    // Renderiza nosso widget
    await tester.pumpWidget(const MyApp());

    // Verifica se o contador é iniciado no 0.
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Pressiona o botão '+' para incrementar o contador e atualiza a tela
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump();

    // Verifica se o contador foi incrementado
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Conseguimos por meio desses testes garantir que a tela da nossa aplicação funcione da maneira correta e assim podemos evitar que caso sejam adicionados mais recursos, essa funcionalidade continue funcionando como o esperado.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

If this article connected with you, consider tapping ❤️ or leaving a brief comment to share your thoughts!

Okay