Como testar um app do Flutter

1. Introdução

O Flutter é um kit de ferramentas de IU do Google para criar apps incríveis e nativos para dispositivos móveis, Web e computadores com uma única base de código.

Neste codelab, você criará e testará um app simples do Flutter. Ele usará o pacote Provider (link em inglês) para gerenciar o estado.

O que você aprenderá

  • Como criar testes usando o framework de testes de widgets.
  • Como criar um teste de integração para testar a interface e o desempenho do app usando a biblioteca integration_test.
  • Como testar classes de dados (provedores) com a ajuda de testes de unidade.

O que você criará

Neste codelab, você começará criando um aplicativo simples com uma lista de itens. Fornecemos o código-fonte para que você consiga pular direto para os testes. O app é compatível com as seguintes operações:

  • Adicionar itens aos favoritos
  • Visualizar a lista de favoritos
  • Remover itens da lista de favoritos

Depois que o app estiver pronto, você criará os seguintes testes:

  • Testes de unidade para validar as operações de inclusão e remoção
  • Testes de widgets para as página inicial e de favoritos
  • Testes de IU e desempenho para o app todo usando testes de integração

GIF do app executado no Android

O que você quer aprender neste codelab?

Ainda não conheço bem o assunto e quero ter uma boa visão geral. Conheço um pouco sobre esse assunto, mas quero me atualizar. Estou procurando exemplos de código para usar no meu projeto. Estou procurando uma explicação de algo específico.

2. Configurar o ambiente de desenvolvimento do Flutter

Você precisa de dois softwares para concluir este laboratório: o SDK do Flutter e um editor (links em inglês).

É possível executar o codelab usando qualquer um destes dispositivos:

  • Um dispositivo físico Android ou iOS conectado ao computador e configurado para o modo de desenvolvedor.
  • O simulador de iOS, que exige a instalação de ferramentas do Xcode.
  • O Android Emulator, que requer configuração no Android Studio.
  • Um navegador (o Chrome é necessário para depuração).
  • Como um aplicativo para computador Windows, Linux ou macOS. Você precisa desenvolver na plataforma em que planeja implantar. Portanto, se quiser desenvolver um app para um computador Windows, você terá que desenvolver no Windows para acessar a cadeia de compilação adequada. Há requisitos específicos de cada sistema operacional que são abordados em detalhes em docs.flutter.dev/desktop (link em inglês).

3. Para começar

Criar um novo app do Flutter e atualizar as dependências

O foco deste codelab é testar um app do Flutter para dispositivos móveis. Você criará rapidamente o app que será testado, usando arquivos de origem que podem ser copiados e colados. O restante do codelab tem o objetivo de ensinar diferentes tipos de testes.

a3c16fc17be25f6c.pngPrograme um app do Flutter simples usando um modelo e seguindo as instruções em Como começar seu primeiro app Flutter (link em inglês) ou na linha de comando, como mostrado a seguir.

$ flutter create testing_app

a3c16fc17be25f6c.pngAdicione dependências de pub na linha de comando. Para fácil gerenciamento do estado, adicione provider:

$ cd testing_app
$ flutter pub add provider
Resolving dependencies...
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ nested 1.0.0
  path 1.8.2 (1.8.3 available)
+ provider 6.0.5
  test_api 0.4.16 (0.4.18 available)
Changed 2 dependencies!

Para testes de direção autônoma do código do Flutter em dispositivos e emuladores, adicione integration_test:

$ flutter pub add --dev --sdk=flutter integration_test
Resolving dependencies...
+ archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ crypto 3.0.2
+ file 6.1.4
+ flutter_driver 0.0.0 from sdk flutter
+ fuchsia_remote_debug_protocol 0.0.0 from sdk flutter
+ integration_test 0.0.0 from sdk flutter
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
+ platform 3.1.0
+ process 4.2.4
+ sync_http 0.3.1
  test_api 0.4.16 (0.4.18 available)
+ typed_data 1.3.1
+ vm_service 9.4.0 (11.0.1 available)
+ webdriver 3.0.1 (3.0.2 available)
Changed 12 dependencies!

Para uma API avançada para testar aplicativos do Flutter executados em dispositivos reais e emuladores, adicione flutter_driver:

$ flutter pub add --dev --sdk=flutter flutter_driver
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test_api 0.4.16 (0.4.18 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!

Para ferramentas gerais de teste, adicione test:

$ flutter pub add --dev test
Resolving dependencies...
+ _fe_analyzer_shared 52.0.0
+ analyzer 5.4.0
  archive 3.3.2 (3.3.6 available)
+ args 2.3.2
  collection 1.17.0 (1.17.1 available)
+ convert 3.1.1
+ coverage 1.6.3
+ frontend_server_client 3.2.0
+ glob 2.1.1
+ http_multi_server 3.2.1
+ http_parser 4.0.2
+ io 1.0.4
  js 0.6.5 (0.6.7 available)
+ logging 1.1.1
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
+ mime 1.0.4
+ node_preamble 2.0.1
+ package_config 2.1.0
  path 1.8.2 (1.8.3 available)
+ pool 1.5.1
+ pub_semver 2.1.3
+ shelf 1.4.0
+ shelf_packages_handler 3.0.1
+ shelf_static 1.1.1
+ shelf_web_socket 1.0.3
+ source_map_stack_trace 2.1.1
+ source_maps 0.10.11
+ test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
+ test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
+ watcher 1.0.2
+ web_socket_channel 2.3.0
  webdriver 3.0.1 (3.0.2 available)
+ webkit_inspection_protocol 1.2.0
+ yaml 3.1.1
Changed 28 dependencies!

Para processar a navegação no app, adicione go_router:

$ flutter pub add go_router
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
+ flutter_web_plugins 0.0.0 from sdk flutter
+ go_router 6.0.4
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Changed 2 dependencies!

As seguintes dependências precisam estar no seu pubspec.yaml:

Em dependencies:

dependencies:
  provider: ^6.0.5
  go_router: ^6.0.4

Em dev_dependencies:

dev_dependencies:
  integration_test:
    sdk: flutter
  flutter_driver:
    sdk: flutter
  test: ^1.22.0

a3c16fc17be25f6c.pngAbra o projeto no editor de código de sua escolha e execute o app. É possível também executar na linha de comando, como mostrado abaixo.

$ flutter run

4. Criar o app

Em seguida, você criará o app para testá-lo. O app contém os seguintes arquivos:

  • lib/models/favorites.dart: cria a classe de modelo para a lista de favoritos.
  • lib/screens/favorites.dart: cria o layout da lista de favoritos.
  • lib/screens/home.dart: cria uma lista de itens.
  • lib/main.dart: arquivo principal em que o app é iniciado.

Primeiro, crie o modelo Favorites em lib/models/favorites.dart

a3c16fc17be25f6c.pngCrie um novo diretório chamado models no diretório lib, depois crie um novo arquivo chamado favorites.dart. Nesse arquivo, adicione o seguinte código:

lib/models/favorites.dart

import 'package:flutter/material.dart';

/// The [Favorites] class holds a list of favorite items saved by the user.
class Favorites extends ChangeNotifier {
  final List<int> _favoriteItems = [];

  List<int> get items => _favoriteItems;

  void add(int itemNo) {
    _favoriteItems.add(itemNo);
    notifyListeners();
  }

  void remove(int itemNo) {
    _favoriteItems.remove(itemNo);
    notifyListeners();
  }
}

Adicionar a página de favoritos ao lib/screens/favorites.dart

a3c16fc17be25f6c.pngCrie um novo diretório chamado screens no diretório lib. Nesse diretório, crie um novo arquivo chamado favorites.dart. Nesse arquivo, adicione o seguinte código:

lib/screens/favorites.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import '../models/favorites.dart';

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

  static String routeName = 'favorites_page';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Favorites'),
      ),
      body: Consumer<Favorites>(
        builder: (context, value, child) => ListView.builder(
          itemCount: value.items.length,
          padding: const EdgeInsets.symmetric(vertical: 16),
          itemBuilder: (context, index) => FavoriteItemTile(value.items[index]),
        ),
      ),
    );
  }
}

class FavoriteItemTile extends StatelessWidget {
  const FavoriteItemTile(this.itemNo, {super.key});

  final int itemNo;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('favorites_text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('remove_icon_$itemNo'),
          icon: const Icon(Icons.close),
          onPressed: () {
            Provider.of<Favorites>(context, listen: false).remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              const SnackBar(
                content: Text('Removed from favorites.'),
                duration: Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Adicionar a página inicial ao lib/screens/home.dart

a3c16fc17be25f6c.png No diretório lib/screens, crie outro arquivo chamado home.dart. No lib/screens/home.dart, adicione o seguinte código:

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/favorites.dart';
import 'favorites.dart';

class HomePage extends StatelessWidget {
  static String routeName = '/';

  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Testing Sample'),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () {
              context.go('/${FavoritesPage.routeName}');
            },
            icon: const Icon(Icons.favorite_border),
            label: const Text('Favorites'),
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: 100,
        cacheExtent: 20.0,
        padding: const EdgeInsets.symmetric(vertical: 16),
        itemBuilder: (context, index) => ItemTile(index),
      ),
    );
  }
}

class ItemTile extends StatelessWidget {
  final int itemNo;

  const ItemTile(this.itemNo, {super.key});

  @override
  Widget build(BuildContext context) {
    var favoritesList = Provider.of<Favorites>(context);

    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.primaries[itemNo % Colors.primaries.length],
        ),
        title: Text(
          'Item $itemNo',
          key: Key('text_$itemNo'),
        ),
        trailing: IconButton(
          key: Key('icon_$itemNo'),
          icon: favoritesList.items.contains(itemNo)
              ? const Icon(Icons.favorite)
              : const Icon(Icons.favorite_border),
          onPressed: () {
            !favoritesList.items.contains(itemNo)
                ? favoritesList.add(itemNo)
                : favoritesList.remove(itemNo);
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(
                content: Text(favoritesList.items.contains(itemNo)
                    ? 'Added to favorites.'
                    : 'Removed from favorites.'),
                duration: const Duration(seconds: 1),
              ),
            );
          },
        ),
      ),
    );
  }
}

Substituir o conteúdo do lib/main.dart

a3c16fc17be25f6c.png Substitua o conteúdo do lib/main.dart pelo seguinte código:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'models/favorites.dart';
import 'screens/favorites.dart';
import 'screens/home.dart';

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

final _router = GoRouter(
  routes: [
    GoRoute(
      path: HomePage.routeName,
      builder: (context, state) {
        return const HomePage();
      },
      routes: [
        GoRoute(
          path: FavoritesPage.routeName,
          builder: (context, state) {
            return const FavoritesPage();
          },
        ),
      ],
    ),
  ],
);

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: MaterialApp.router(
        title: 'Testing Sample',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          useMaterial3: true,
        ),
        routerConfig: _router,
      ),
    );
  }
}

O app está concluído, mas ainda não foi testado.

a3c16fc17be25f6c.pngExecute o app. Ele vai ficar parecido com a seguinte captura de tela:

b74f843e42a28b0f.png

O app exibe uma lista de itens. Toque no ícone em formato de coração em qualquer linha para preencher o coração e adicionar o item à lista de favoritos. O botão Favorites na AppBar leva a uma segunda tela que contém a lista de favoritos.

Agora o app está pronto para ser testado. Você vai começar os testes na próxima etapa.

5. Testes de unidade do provedor

Comece fazendo um teste de unidade do modelo favorites. O que é esse teste? Um teste de unidade verifica se cada unidade do software, seja ela uma função, objeto ou widget, executa a tarefa pretendida de modo correto.

Todos os arquivos de teste de um app do Flutter, exceto os testes de integração, são colocados no diretório test.

Remover o test/widget_test.dart

a3c16fc17be25f6c.png Antes de iniciar o teste, exclua o arquivo widget_test.dart. Você adicionará seus próprios arquivos de teste.

Criar um novo arquivo de teste

Primeiro, você testará o método add() no modelo Favorites para verificar se um novo item foi adicionado à lista e se ela reflete essa mudança. Por convenção, a estrutura no diretório test imita a do diretório lib e os arquivos Dart têm o mesmo nome, mas com o anexo _test.

a3c16fc17be25f6c.png Crie um diretório models no test. Nesse novo diretório, crie um arquivo favorites_test.dart com o seguinte conteúdo:

test/models/favorites_test.dart

import 'package:test/test.dart';
import 'package:testing_app/models/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });
  });
}

O framework de testes do Flutter permite vincular testes semelhantes, que são relacionados entre si, em um grupo. Podem existir vários grupos em um único arquivo de teste destinados a testar partes diferentes do arquivo correspondente no diretório lib.

O método test() usa dois parâmetros de posicionamento: a description do teste e o callback em que o teste é criado.

a3c16fc17be25f6c.png Teste a remoção de um item da lista. Insira o teste a seguir no mesmo grupo Testing App Provider:

test/models/favorites_test.dart

test('An item should be removed', () {
  var number = 45;
  favorites.add(number);
  expect(favorites.items.contains(number), true);
  favorites.remove(number);
  expect(favorites.items.contains(number), false);
});

Executar o teste

a3c16fc17be25f6c.png Na linha de comando, navegue até o diretório raiz do projeto e digite o seguinte comando:

$ flutter test test/models/favorites_test.dart

Se tudo funcionar, você verá uma mensagem parecida com a seguinte:

00:06 +2: All tests passed!

Veja o arquivo de teste completo: test/models/favorites_test.dart (link em inglês).

Para ver mais informações sobre o teste de unidade, acesse Introdução ao teste de unidade (link em inglês).

6. Teste de widget

Nesta etapa, você vai adicionar código para testar widgets. Os testes de widget são exclusivos do Flutter. Com eles, é possível testar cada widget isoladamente. Nesta etapa, as telas HomePage e FavoritesPage serão testadas individualmente.

O teste de widget usa a função testWidget(), e não a test(). Assim como a função test(), a testWidget() recebe dois parâmetros: description, e callback. Porém, o callback recebe um WidgetTester como argumento.

Os testes de widget usam a TestFlutterWidgetsBinding, uma classe que oferece os mesmos recursos que os widgets teriam em um app em execução, como informações sobre o tamanho da tela e a capacidade de programar animações, mas sem executar no app real. Em vez disso, um ambiente virtual é usado para instanciar o widget e, depois, testar os resultados. Aqui, o pumpWidget inicia o processo instruindo o framework a ativar e medir um widget específico, da mesma forma que seria feito em um aplicativo.

O framework de teste de widget fornece finders para encontrar widgets, como text(), byType() e byIcon().. O framework também fornece matchers para analisar os resultados.

Comece testando o widget HomePage.

Criar um novo arquivo de teste

O primeiro teste verifica se a rolagem da HomePage funciona corretamente.

a3c16fc17be25f6c.png Crie um novo arquivo no diretório test e nomeie-o como home_test.dart. Adicione o seguinte código no arquivo criado:

test/home_test.dart (link em inglês)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/home.dart';

Widget createHomeScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) => Favorites(),
      child: const MaterialApp(
        home: HomePage(),
      ),
    );

void main() {
  group('Home Page Widget Tests', () {
    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
  });
}

A função createHomeScreen() é usada para criar um app que carrega o widget a ser testado em um MaterialApp, envolvido em um ChangeNotifierProvider. O widget da página inicial precisa que os dois estejam presentes acima dele na árvore de widgets, para que ele consiga herdar e ter acesso aos dados fornecidos por eles. Essa função é transmitida como um parâmetro para a função pumpWidget().

Em seguida, teste se o framework consegue encontrar uma ListView renderizada na tela.

a3c16fc17be25f6c.png Adicione o seguinte snippet de código a home_test.dart:

test/home_test.dart (link em inglês)

group('Home Page Widget Tests', () {

  // BEGINNING OF NEW CONTENT
  testWidgets('Testing if ListView shows up', (tester) async {
    await tester.pumpWidget(createHomeScreen());
    expect(find.byType(ListView), findsOneWidget);
  });
  // END OF NEW CONTENT

    testWidgets('Testing Scrolling', (tester) async {
      await tester.pumpWidget(createHomeScreen());
      expect(find.text('Item 0'), findsOneWidget);
      await tester.fling(
        find.byType(ListView),
        const Offset(0, -200),
        3000,
      );
      await tester.pumpAndSettle();
      expect(find.text('Item 0'), findsNothing);
    });
});

Executar o teste

Primeiro, execute o teste da mesma maneira que você faria com um teste de unidade, com o comando:

$ flutter test test/home_test.dart

O teste é rápido, e uma mensagem como esta será exibida:

00:02 +2: All tests passed!

Você também pode testar widgets usando um dispositivo ou emulador, que permite assistir à execução do teste. Isso também possibilita usar a recarga dinâmica.

a3c16fc17be25f6c.png Conecte o dispositivo ou inicie o emulador. Também é possível executar o teste como um aplicativo de computador.

a3c16fc17be25f6c.png Na linha de comando, navegue até o diretório raiz do projeto e digite o seguinte comando:

$ flutter run test/home_test.dart

Pode ser necessário selecionar o dispositivo em que será executado o teste. Nesse caso, siga as instruções e selecione um dispositivo:

Multiple devices found:
Linux (desktop) • linux  • linux-x64      • Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)    • chrome • web-javascript • Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"):

Se tudo funcionar, você receberá uma resposta parecida com a seguinte:

Launching test/home_test.dart on Linux in debug mode...
Building Linux application...
flutter: 00:00 +0: Home Page Widget Tests Testing if ListView shows up
Syncing files to device Linux...                                    62ms

Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on Linux is available at: http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:00 +1: Home Page Widget Tests Testing Scrolling
The Flutter DevTools debugger and profiler on Linux is available at:
http://127.0.0.1:9100?uri=http://127.0.0.1:35583/GCpdLBqf2UI=/
flutter: 00:02 +2: All tests passed!

Em seguida, faça mudanças no arquivo de teste e pressione Shift + R para fazer uma recarga dinâmica do app. Depois, execute todos os testes de novo. Não pare o aplicativo.

a3c16fc17be25f6c.png Adicione mais testes ao grupo que testa os widgets da página inicial. Copie o teste a seguir e cole no arquivo:

test/home_test.dart (link em inglês)

testWidgets('Testing IconButtons', (tester) async {
  await tester.pumpWidget(createHomeScreen());
  expect(find.byIcon(Icons.favorite), findsNothing);
  await tester.tap(find.byIcon(Icons.favorite_border).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Added to favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsWidgets);
  await tester.tap(find.byIcon(Icons.favorite).first);
  await tester.pumpAndSettle(const Duration(seconds: 1));
  expect(find.text('Removed from favorites.'), findsOneWidget);
  expect(find.byIcon(Icons.favorite), findsNothing);
});

Esse teste verifica se, com um toque, o IconButton muda de Icons.favorite_border (coração vazio) para Icons.favorite (coração preenchido) e volta para Icons.favorite_border com outro toque.

a3c16fc17be25f6c.png Digite Shift + R. Isso faz uma recarga dinâmica do app e executa todos os apps novamente.

Veja o arquivo de teste completo em: test/home_test.dart. (link em inglês).

a3c16fc17be25f6c.png Use o mesmo processo para testar a FavoritesPage com o código a seguir. Siga as mesmas etapas e execute o teste.

test/favorites_test.dart (link em inglês)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

Este teste verifica se um item desaparece quando o botão Fechar (remover) é pressionado.

Para ver mais informações sobre testes de widgets, acesse (links em inglês):

7. Como testar a interface do app com testes de integração

Os testes de integração são usados para testar a forma como as partes individuais de um app funcionam em conjunto. A biblioteca integration_test (link em inglês) é usada para fazer testes de integração no Flutter. Essa é a versão do Flutter para o Selenium WebDriver, Protractor, Espresso ou Earl Gray. O pacote usa flutter_driver (link em inglês) internamente para controlar o teste em um dispositivo.

A criação de testes de integração no Flutter é semelhante à criação de testes para widgets, exceto que os testes de integração são executados em um dispositivo móvel, navegador ou aplicativo de computador, chamado de dispositivo de destino.

Criar o teste

a3c16fc17be25f6c.pngCrie um diretório chamado integration_test no diretório raíz do projeto e, dentro dele, um arquivo novo chamado app_test.dart.

integration_test/app_test.dart (link em inglês)

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App', () {
    testWidgets('Favorites operations test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final iconKeys = [
        'icon_0',
        'icon_1',
        'icon_2',
      ];

      for (var icon in iconKeys) {
        await tester.tap(find.byKey(ValueKey(icon)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Added to favorites.'), findsOneWidget);
      }

      await tester.tap(find.text('Favorites'));
      await tester.pumpAndSettle();

      final removeIconKeys = [
        'remove_icon_0',
        'remove_icon_1',
        'remove_icon_2',
      ];

      for (final iconKey in removeIconKeys) {
        await tester.tap(find.byKey(ValueKey(iconKey)));
        await tester.pumpAndSettle(const Duration(seconds: 1));

        expect(find.text('Removed from favorites.'), findsOneWidget);
      }
    });
  });
}

Executar o teste

a3c16fc17be25f6c.png Conecte o dispositivo ou inicie o emulador. Também é possível executar o teste como um aplicativo de computador.

a3c16fc17be25f6c.png Na linha de comando, navegue até o diretório raiz do projeto e digite o seguinte comando:

$ flutter test integration_test/app_test.dart

Se tudo funcionar, você vai receber uma resposta parecida com a seguinte:

Multiple devices found:
Linux (desktop)  linux   linux-x64       Ubuntu 22.04.1 LTS 5.15.0-58-generic
Chrome (web)     chrome  web-javascript  Google Chrome 109.0.5414.119
[1]: Linux (linux)
[2]: Chrome (chrome)
Please choose one (To quit, press "q/Q"): 1
00:00 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart                                                B00:08 +0: loading /home/miquel/tmp/testing_app/integration_test/app_test.dart
00:26 +1: All tests passed!

8. Como testar o desempenho do app com o driver do Flutter

Criar um teste de desempenho

Crie um novo arquivo de teste chamado perf_test.dart na pasta integration_test com o seguinte conteúdo:

integration_test/perf_test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:testing_app/main.dart';

void main() {
  group('Testing App Performance', () {
    final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;

    testWidgets('Scrolling test', (tester) async {
      await tester.pumpWidget(const TestingApp());

      final listFinder = find.byType(ListView);

      await binding.traceAction(() async {
        await tester.fling(listFinder, const Offset(0, -500), 10000);
        await tester.pumpAndSettle();

        await tester.fling(listFinder, const Offset(0, 500), 10000);
        await tester.pumpAndSettle();
      }, reportKey: 'scrolling_summary');
    });
  });
}

A função ensureInitialized() verifica se o driver do teste de integração foi inicializado e reinicia o driver, se necessário. Configurar framePolicy como fullyLive é bom para testar código animado.

Este teste percorre a lista de itens com muita rapidez e, em seguida, rola a página para cima até o início. A função traceAction() registra as ações e gera um resumo da linha do tempo.

Capturar os resultados do desempenho

Para capturar os resultados, crie uma pasta chamada test_driver, com um arquivo perf_driver.dart e adicione o seguinte código:

test_driver/perf_driver.dart

import 'package:flutter_driver/flutter_driver.dart' as driver;
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() {
  return integrationDriver(
    responseDataCallback: (data) async {
      if (data != null) {
        final timeline = driver.Timeline.fromJson(
            data['scrolling_summary'] as Map<String, dynamic>);

        final summary = driver.TimelineSummary.summarize(timeline);

        await summary.writeTimelineToFile(
          'scrolling_summary',
          pretty: true,
          includeSummary: true,
        );
      }
    },
  );
}

Executar o teste

a3c16fc17be25f6c.png Conecte o dispositivo ou inicie o emulador.

a3c16fc17be25f6c.png Na linha de comando, navegue até o diretório raiz do projeto e digite o seguinte comando:

$ flutter drive \
  --driver=test_driver/perf_driver.dart \
  --target=integration_test/perf_test.dart \
  --profile \
  --no-dds

Se tudo funcionar, você vai receber uma resposta parecida com a seguinte:

Running "flutter pub get" in testing_app...
Resolving dependencies...
  archive 3.3.2 (3.3.6 available)
  collection 1.17.0 (1.17.1 available)
  js 0.6.5 (0.6.7 available)
  matcher 0.12.13 (0.12.14 available)
  meta 1.8.0 (1.9.0 available)
  path 1.8.2 (1.8.3 available)
  test 1.22.0 (1.23.0 available)
  test_api 0.4.16 (0.4.18 available)
  test_core 0.4.20 (0.4.23 available)
  vm_service 9.4.0 (11.0.1 available)
  webdriver 3.0.1 (3.0.2 available)
Got dependencies!
Running Gradle task 'assembleProfile'...                         1,379ms
✓  Built build/app/outputs/flutter-apk/app-profile.apk (14.9MB).
Installing build/app/outputs/flutter-apk/app-profile.apk...        222ms
I/flutter ( 6125): 00:04 +1: Testing App Performance (tearDownAll)
I/flutter ( 6125): 00:04 +2: All tests passed!
All tests passed.

Quando o teste estiver concluído, o diretório de build na raiz do projeto terá dois arquivos:

  1. scrolling_summary.timeline_summary.json contém o resumo. Abra o arquivo com qualquer editor de texto para revisar as informações.
  2. scrolling_summary.timeline.json contém os dados completos da linha do tempo.

Para mais detalhes sobre o teste de integração, visite:

9. Parabéns!

Você concluiu o codelab e aprendeu formas diferentes de testar um app do Flutter.

O que você aprendeu

  • Como testar provedores com a ajuda dos testes de unidade
  • Como testar widgets usando o framework de testes de widgets.
  • Como testar a IU do app usando testes de integração.
  • Como testar o desempenho do app usando testes de integração.

Para saber mais sobre os testes no Flutter, acesse (links em inglês):