O Flutter é lembrado quando é necessário criar rapidamente um aplicativo bonito e responsivo para várias plataformas ao mesmo tempo, mas como garantir a qualidade do código "rápido"?
Você ficará surpreso, mas o Flutter tem meios para não apenas garantir a qualidade do código, mas também garantir a operacionalidade da interface visual.
No artigo, examinaremos como estão as coisas com os testes no Flutter, analisaremos os testes de widgets e os testes de integração do aplicativo como um todo.

Comecei a estudar o Flutter há mais de um ano, antes de seu lançamento oficial, durante o estudo não foi um problema encontrar informações de desenvolvimento. E quando eu queria experimentar o TDD , as informações sobre os testes eram desastrosamente pequenas. Em russo e, em geral, quase nenhum. Os problemas de teste tiveram que ser estudados de forma independente, de acordo com o código fonte dos testes de Flutter e de artigos raros em inglês. Tudo o que estudei sobre o teste de elementos visuais, descrevi em um artigo para ajudar aqueles que estão apenas começando a se aprofundar no tópico.
Informação geral
Um teste de widget testa um único widget. Também pode ser chamado de teste de componente. O objetivo do teste é provar que a interface do usuário do widget parece e interage conforme o planejado. O teste de um widget requer um ambiente de teste que forneça o contexto apropriado para o ciclo de vida do widget.
O widget testado tem a capacidade de receber ações e eventos do usuário e responder a eles, construir uma árvore de widgets filhos. Portanto, os testes de widget são mais complexos que os testes de unidade. No entanto, como o teste de unidade, o ambiente de teste de widget é uma simulação simples, muito mais simples que um sistema de interface de usuário completo.
O teste de widget permite isolar e testar o comportamento de um único elemento da interface visual. E, o que é digno de nota, para realizar todas as verificações no console, o que é ideal para testes executados como parte do processo de CI / CD.
Os arquivos que contêm testes geralmente estão localizados no subdiretório de teste do projeto.
Os testes podem ser executados no IDE ou no console com o comando:
$ flutter test
Nesse caso, todos os testes com a máscara * _test.dart do subdiretório test serão executados.
Você pode executar um teste separado especificando o nome do arquivo:
$ flutter test test/phone_screen_test.dart
O teste é criado pela função testWidgets , que recebe uma ferramenta como parâmetro do testador , com a qual o código de teste interage com o widget em teste:
testWidgets(' ', (WidgetTester tester) async {
Para combinar testes em blocos lógicos, as funções de teste podem ser combinadas em grupos, dentro da função de grupo :
group(' ', (){ testWidgets(' ', (WidgetTester tester) async {
As funções setUp e tearDown permitem executar algum código "antes" e "depois" de cada teste. Assim, as funções setUpAll e tearDownAll permitem executar o código "antes" e "depois" de todos os testes, e se essas funções forem chamadas dentro do grupo, elas serão chamadas "antes" e "depois" da execução de todos os testes no grupo:
setUp(() {
Pesquisa de widget
Para executar alguma ação em um widget aninhado, é necessário encontrá-lo na árvore de widgets. Para fazer isso, existe um objeto de localização global que permite encontrar widgets:
- na árvore por texto - find.text , find.widgetWithText ;
- pela chave - find.byKey ;
- por ícone - find.byIcon , find.widgetWithIcon ;
- por tipo - find.byType ;
- pela posição na árvore - find.descendant e find.ancestor ;
- usando uma função que analisa widgets em uma lista - find.byWidgetPredicate .
Interação do widget de teste
A classe WidgetTester fornece funções para criar um widget de teste, aguardar a alteração do estado e executar algumas ações nesses widgets.
Qualquer alteração no widget causa uma alteração no seu estado. Mas o ambiente de teste não reconstrói o widget ao mesmo tempo. Você deve indicar independentemente ao ambiente de teste que deseja reconstruir o widget chamando as funções pump ou pumpAndSettle .
- pumpWidget - crie um widget de teste;
- pump - inicia o processamento da transição de estado do widget e aguarda a conclusão dentro do tempo limite especificado (100 ms por padrão);
- pumpAndSettle - chama a bomba em um ciclo para alterar os estados durante um determinado tempo limite (100 ms por padrão); esta é a espera para que todas as animações sejam concluídas;
- toque - envie um clique para o widget;
- longPress - pressão longa;
- arremesso - furto / furto;
- arrastar - transferir;
- enterText - entrada de texto.
Os testes podem implementar cenários positivos, verificar oportunidades planejadas e negativos, para garantir que eles não levem a consequências fatais, por exemplo, quando um usuário clica na direção errada e não digita o que é necessário:
await tester.enterText(find.byKey(Key('phoneField')), 'bla-bla-bla');
Após qualquer ação com widgets, você precisa chamar tester.pumpAndSettle () para alterar os estados.
Moki
Muitos estão familiarizados com a biblioteca Mockito . Essa biblioteca do mundo Java acabou sendo tão bem-sucedida que existem implementações dessa biblioteca em muitas linguagens de programação, incluindo o Dart.
Para se conectar, você deve adicionar a dependência ao projeto. Adicione as seguintes linhas ao arquivo pubspec.yaml :
dependencies: mockito: any
E conecte-se no arquivo de teste:
import 'package:mockito/mockito.dart';
Essa biblioteca permite criar classes moque, das quais o widget testado depende, para que o teste seja mais simples e cubra apenas o código que estamos testando.
Por exemplo, se testarmos o widget PhoneInputScreen , que, quando clicado, usando o serviço AuthInteractor , executa uma solicitação ao back-end authInteractor.checkAccess () e , em seguida, substituindo a simulação em vez do serviço, podemos verificar a coisa mais importante - o fato de acessar esse serviço.
Os mobs de dependência são criados como descendentes da classe Mock e implementam a interface de dependência:
class AuthInteractorMock extends Mock implements AuthInteractor {}
Uma classe no Dart também é uma interface, portanto, não há necessidade de declarar a interface separadamente, como em outras linguagens de programação.
Para determinar a funcionalidade do mok, a função when é usada, o que permite determinar a resposta do mok à chamada de uma função:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.value(true));
Moki pode retornar erros ou dados errados:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.error(UnknownHttpStatusCode(null)));
Cheques
Durante o teste, você pode verificar se há widgets na tela. Isso permite que você verifique se o novo estado da tela está correto em termos de visibilidade dos widgets desejados:
expect(find.text(' '), findsOneWidget); expect(find.text(' '), findsNothing);
Após a conclusão do teste, você também pode verificar quais métodos da classe mob foram chamados durante o teste e quantas vezes. Isso é necessário, por exemplo, para entender se esses ou esses dados são solicitados com muita frequência, se há alterações desnecessárias no estado do aplicativo:
verify(appComponent.authInteractor).called(1); verify(authInteractor.checkAccess(any)).called(1); verifyNever(appComponent.profileInteractor);
Depuração
Os testes são realizados no console sem nenhum gráfico. Você pode executar testes no modo de depuração e definir pontos de interrupção no código do widget.
Para ter uma idéia do que está acontecendo na árvore de widgets, você pode usar a função debugDumpApp () , que, quando chamada no código de teste, exibe a representação textual da hierarquia de toda a árvore de widgets em um determinado momento no console.
Para entender como o widget usa o moki, há uma função logInvocations () . Toma como parâmetro uma lista de moxas e emite para o console uma sequência de métodos que chama essas moxas que foram realizadas no teste.
Um exemplo dessa conclusão está abaixo. A marca VERIFIED está nas chamadas que foram verificadas no teste usando a função de verificação :
AppComponentMock.sessionChangedInteractor [VERIFIED] AppComponentMock.authInteractor [VERIFIED] AuthInteractorMock.checkAccess(71111111111)
Preparação
Todas as dependências devem ser enviadas ao widget testado na forma de mok:
class SomeComponentMock extends Mock implements SomeComponent {} class AuthInteractorMock extends Mock implements AuthInteractor {}
A transferência de dependências para o componente testado deve ser realizada de alguma maneira aceita na sua aplicação. Para simplificar a narrativa, considere um exemplo em que as dependências são passadas pelo construtor.
No exemplo de código, PhoneInputScreen é um widget de teste baseado em StatefulWidget envolvido no Scaffold . Ele é criado em um ambiente de teste usando a função pumpWidget () :
await tester.pumpWidget(PhoneInputScreen(mock));
No entanto, um widget real pode usar o alinhamento para widgets aninhados, o que exige o MediaQuery na árvore de widgets, provavelmente obtém Navigator.of (context) para navegação, portanto, é mais prático agrupar o widget em teste no MaterialApp ou no CupertinoApp :
await tester.pumpWidget( MaterialApp( home: PhoneInputScreen(mock), ), );
Depois de criar um widget de teste e depois de qualquer ação com ele, você precisa chamar tester.pumpAndSettle () para que o ambiente de teste lide com todas as alterações no estado do widget.
Testes de integração
Informação geral
Diferentemente dos testes de widget, o teste de integração verifica o aplicativo inteiro ou parte dele. O objetivo do teste de integração é garantir que todos os widgets e serviços funcionem juntos conforme o esperado. A operação do teste de integração pode ser observada no simulador ou na tela do dispositivo. Este método é um bom substituto para o teste manual. Além disso, testes de integração podem ser usados para testar o desempenho do aplicativo.
O teste de integração geralmente é realizado em um dispositivo ou emulador real, como o iOS Simulator ou Android Emulator.
Arquivos que contêm testes de integração geralmente estão localizados no subdiretório test_driver do projeto.
O aplicativo é isolado do código do driver de teste e inicia após ele. O driver de teste permite controlar o aplicativo durante o teste. É assim:
import 'package:flutter_driver/driver_extension.dart'; import 'package:app_package_name/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); }
Os testes são executados na linha de comando. Se o lançamento do aplicativo de destino for descrito no arquivo app.dart e o script de teste for chamado app_test.dart , o seguinte comando será suficiente:
$ flutter drive --target=test_driver/app.dart
Se o script de teste tiver um nome diferente, será necessário especificá-lo explicitamente:
$ flutter drive --target=test_driver/app.dart --driver=test_driver/home_test.dart
Um teste é criado pela função de teste e agrupado pela função de grupo .
group('park-flutter app', () {
Este exemplo mostra o código para criar um driver de teste através do qual os testes interagem com o aplicativo em teste.
Interação com o aplicativo testado
A ferramenta FlutterDriver interage com o aplicativo de teste através dos seguintes métodos:
- toque - envie um clique para o widget;
- waitFor - aguarde o widget aparecer na tela;
- waitForAbsent - aguarde o widget desaparecer;
- scroll e scrollIntoView , scrollUntilVisible - role a tela para o deslocamento especificado ou para o widget desejado;
- enterText , getText - insira o texto ou pegue o texto do widget;
- captura de tela - obtenha uma captura de tela;
- requestData - interação mais complexa através de uma chamada de função dentro do aplicativo em teste.
Pode haver uma situação em que você precisa influenciar o estado global do aplicativo a partir do código de teste. Por exemplo, para simplificar o teste de integração, substituindo parte dos serviços no aplicativo pelo moki. No aplicativo, você pode especificar um manipulador de solicitação, que pode ser acessado por meio de uma chamada para driver.requestData ('some param') no código de teste:
void main() { Future<String> dataHandler(String msg) async { if (msg == "some param") {
Pesquisa de widget
A procura de widgets durante o teste de integração com o objeto de localização global difere na composição dos métodos da funcionalidade semelhante nos testes de widgets. No entanto, o significado geral praticamente não muda:
- na árvore por texto - find.text , find.widgetWithText ;
- pela chave - find.byValueKey ;
- por tipo - find.byType ;
- no prompt - find.byTooltip ;
- por rótulo semântico - find.bySemanticsLabel ;
- por posição na árvore find.descendant e find.ancestor .
Conclusão
Vimos maneiras de organizar o teste de uma interface de aplicativo escrita usando Flutter. Podemos implementar testes para verificar se o código atende aos requisitos das especificações técnicas e fazer testes com essa mesma tarefa. Das deficiências observadas nos testes de integração - não há como interagir com os diálogos do sistema da plataforma. Mas, por exemplo, as solicitações de permissões podem ser evitadas emitindo permissões da linha de comandos no estágio de instalação do aplicativo, conforme descrito neste ticket .
Este artigo é o ponto de partida para explorar um tópico de teste que apresenta brevemente ao leitor como o teste de interface do usuário funciona. Ele não salva a leitura da documentação, da qual é fácil descobrir como uma determinada classe ou método funciona. Afinal, o estudo de um novo tópico requer, antes de tudo, uma compreensão de todos os processos em andamento como um todo, sem detalhes excessivos.