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),
),
);
}
}
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);
});
}
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
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
## main()
void main() {
..
}
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 {
...
});
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', () {
...
});
## tester.pumpWidget()
await tester.pumpWidget(const MyApp());
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()));
## expect()
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
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);
> Espero que seja encontrado apenas um widget com o texto '0'
>
expect(find.text('1'), findsNothing);
> Espero que não seja encontrado nenhum um widget com o texto '1'
## tester.tap()
await tester.tap(find.byIcon(Icons.add));
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);
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();
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();
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));
Ou podemos usar o `pumpAndSettle()` que vai esperar pelo próximo frame para continuar o teste.
await tester.pumpAndSettle();
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);
});
}
## 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.