Acho que muitas pessoas já estão familiarizadas com o Flutter e, pelo menos por interesse, começaram aplicativos simples nele. Chegou a hora de garantir que tudo funcione conforme necessário e os testes de integração nos ajudarão nisso.

Os testes de integração no Flutter são escritos usando o Flutter Driver, para o qual existe um tutorial simples e compreensível no site oficial . Em sua estrutura, esses testes são semelhantes ao Espresso do mundo Android. Primeiro, você precisa encontrar os elementos da interface do usuário na tela:
final SerializableFinder button = find.byValueKey("button");
em seguida, execute algumas ações com eles:
driver = await FlutterDriver.connect(); ... await driver.tap(button);
e verifique se os elementos de interface do usuário necessários estão no estado desejado:
final SerializableFinder text = find.byValueKey("text"); expect(await driver.getText(text), "some text");
Com um exemplo simples, é claro, tudo parece elementar. Mas com o crescimento do aplicativo em teste e o aumento no número de testes, não quero duplicar a pesquisa de elementos da interface do usuário antes de cada teste. Além disso, você precisará estruturar esses elementos da interface do usuário, pois pode haver muitas telas. Para fazer isso, torne os testes de escrita mais convenientes.
Objetos de tela
No Android ( Kakao ), esse problema é resolvido agrupando elementos da interface do usuário de cada tela em Screen (Page-Object). Uma abordagem semelhante pode ser aplicada aqui, com a exceção de que no Flutter, para executar ações com elementos da interface do usuário, você precisa não apenas do Finder
(para procurar um elemento da interface do usuário), mas também do FlutterDriver
(para executar uma ação); portanto, é necessário armazenar um link para o FlutterDriver
no Screen
.
Para definir cada elemento da interface do usuário, adicionamos a classe DWidget
(neste caso, D - da palavra Dart). Para criar um DWidget
precisará do FlutterDriver
, com o qual as ações serão executadas nesse elemento da interface do usuário, bem como do ValueKey
, que coincide com o Flutter do ValueKey
do widget do aplicativo com o qual queremos interagir:
class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; DWidget(this._driver, dynamic valueKey) : _finder = find.byValueKey(valueKey); ...
find.byValueKey(…)
ao criar manualmente cada DWidget
inconveniente; portanto, é melhor passar o valor ValueKey
para o ValueKey
, e o próprio DWidget
obterá o SerializableFinder
desejado. Também não é muito conveniente transmitir manualmente o FlutterDriver
ao criar cada DWidget
, para que você possa armazenar o FlutterDriver
no BaseScreen
e transferi-lo para o DWidget
, além de adicionar um novo método ao BaseScreen
para criar o BaseScreen
:
abstract class BaseScreen { final FlutterDriver _driver; BaseScreen(this._driver); DWidget dWidget(dynamic key) => DWidget(_driver, key); ...
Assim, criar classes de telas e obter elementos de interface do usuário nelas será muito mais fácil:
class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get button => dWidget('button'); DWidget get textField => dWidget('text_field'); ... }
Livrar-se de await
Outra coisa não muito conveniente ao escrever testes com o FlutterDriver
é a necessidade de await
antes de cada ação:
await driver.tap(button); await driver.scrollUntilVisible(list, checkBox); await driver.tap(checkBox); await driver.tap(text); await driver.enterText("some text");
Esquecer a await
é fácil e, sem ela, os testes não funcionarão corretamente, porque os métodos do driver
retornam Future<void>
e, quando são chamados sem await
são executados até a primeira await
dentro do método, e o restante do método é "adiado para mais tarde".
Você pode TestAction
isso criando um TestAction
que "encapsulará" o Future
para que possamos esperar até que uma ação seja concluída antes de passar para a próxima:
typedef TestAction = Future<void> Function();
(essencialmente, TestAction
é qualquer função (ou lambda) que retorna um Future<void>
)
Agora você pode executar facilmente a sequência TestAction
sem espera desnecessária:
Future<void> runTestActions(Iterable<TestAction> actions) async { for (final action in actions) { await action(); } }
DWidget
usado para interagir com os elementos da interface do usuário e será muito conveniente se essas ações forem TestAction
para que possam ser usadas no método runTestAction
. Para fazer isso, a classe DWidget
terá métodos de ação:
class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; ... TestAction tap({Duration timeout}) => () => _driver.tap(_finder, timeout: timeout); TestAction setText(String text, {Duration timeout}) => () async { await _driver.tap(_finder, timeout: timeout); await _driver.enterText(text ?? "", timeout: timeout); }; ... }
Agora você pode escrever testes da seguinte maneira:
class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get field_1 => dWidget('field_1'); DWidget get field_2 => dWidget('field_2'); DWidget field2Variant(int i) => dWidget('variant_$i'); DWidget get result => dWidget('result'); } … final mainScreen = MainScreen(driver); await runTestActions([ mainScreen.result.hasText("summa = 0"), mainScreen.field_1.setText("3"), mainScreen.field_2.tap(), mainScreen.field2Variant(2).tap(), mainScreen.result.hasText("summa = 5"), ]);
Se você precisar executar alguma ação em runTestActions
que não esteja relacionada ao DWidget
, será necessário criar uma lambda que retorne um Future<void>
:
await runTestActions([ mainScreen.result.hasText("summa = 0"), () => driver.requestData("some_message"), () async => print("some_text"), mainScreen.field_1.setText("3"), ]);
FlutterDriverHelper
FlutterDriver
possui vários métodos para interagir com elementos da interface do usuário (pressionar, receber e inserir texto, rolagem etc.) e, para esses métodos, o DWidget
possui métodos correspondentes que retornam TestAction
.
Por conveniência, todo o código descrito neste artigo é publicado como a biblioteca FlutterDriverHelper em pub.dev .
Para rolar pelas listas nas quais os elementos são criados dinamicamente (por exemplo, ListView.builder
), o FlutterDriver
possui um método scrollUntilVisible
:
Future<void> scrollUntilVisible( SerializableFinder scrollable, SerializableFinder item, { double alignment = 0.0, double dxScroll = 0.0, double dyScroll = 0.0, Duration timeout, }) async { ... }
Esse método rola o widget rolável na direção especificada até que o widget de item
apareça na tela (ou até que timeout
um timeout
). Para não passar scrollable
em cada rolagem, a classe DScrollItem foi adicionada, que herda um DWidget
e representa um item da lista. Ele contém um link para scrollable
; portanto, ao rolar, resta apenas especificar dyScroll
ou dxScroll
:
class SecondScreen extends BaseScreen { SecondScreen(FlutterDriver driver) : super(driver); DWidget get list => dWidget("list"); DScrollItem item(int index) => dScrollItem('item_$index', list); } ... final secondScreen = SecondScreen(driver); await runTestActions([ secondScreen.item(42).scrollUntilVisible(dyScroll: -300), ... ]);
Durante os testes, você pode capturar imagens do aplicativo e o FlutterDriverHelper
possui um FlutterDriverHelper
Screenshoter
que salva as capturas de tela na pasta desejada com o horário atual e pode trabalhar com o TestAction
.
Outros problemas e suas soluções
- Não consegui encontrar uma maneira padrão de clicar nos botões nas caixas de diálogo de hora / data - tenho que usar o
TestHooks
. TestHooks
também pode ser útil para alterar a hora / data atual durante o teste. - Na lista suspensa de
DropdownButtonFormField
você precisa especificar a key
não para DropdownMenuItem
, mas para o child
desse DropdownMenuItem
; caso contrário, o Flutter Driver
não poderá encontrá-lo. Além disso, a rolagem na lista suspensa ainda não funciona ( problema no github.com ). - o método
FlutterDriver.getCenter
retorna Future<DriverOffset>
, mas DriverOffset
não faz parte da API pública ( problema no github.com ) - Existem algumas coisas mais problemáticas e não óbvias que já existem. Você pode ler sobre eles em um artigo maravilhoso . Particularmente útil foi a capacidade de executar testes na área de trabalho e redefinir o estado do aplicativo antes do início de cada teste.
- Você pode executar testes usando as ações do Github. Mais detalhes aqui .
Todo
Os futuros FlutterDriverHelper
do FlutterDriverHelper
incluem:
- rolagem automática para o item da lista desejado se, no momento do acesso, não estiver visível na tela (como é feito na biblioteca Kaspresso para Android). Se possível, mesmo nas duas direções.
- interceptores para ações executadas com um
Dwidget
ou DscrollItem
.
Comentários e feedback construtivo são bem-vindos.
Atualização (15/01/2020) : na versão 1.1.0, TestAction
se tornou uma classe, com o campo String name
da String name
. E, graças a isso, foi runTestActions
log de todas as ações executadas no método runTestActions
.