Testes de integração de vibração - é fácil

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(); } } 

Usando TestAction em um DWidget


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 .

Source: https://habr.com/ru/post/pt483468/


All Articles