Pruebas de integración de flutter: es fácil

Creo que muchas personas ya están familiarizadas con Flutter y, al menos por interés, comenzaron aplicaciones simples en él. Es hora de asegurarse de que todo funcione como lo necesitan, y las pruebas de integración nos ayudarán con esto.


Las pruebas de integración en Flutter se escriben utilizando el controlador de Flutter, para lo cual hay un tutorial simple e intuitivo en el sitio web oficial . En su estructura, tales pruebas son similares a Espresso del mundo de Android. Primero necesita encontrar los elementos de la interfaz de usuario en la pantalla:


final SerializableFinder button = find.byValueKey("button"); 

luego realiza algunas acciones con ellos:


 driver = await FlutterDriver.connect(); ... await driver.tap(button); 

y verifique que los elementos de la IU requeridos estén en el estado deseado:


 final SerializableFinder text = find.byValueKey("text"); expect(await driver.getText(text), "some text"); 

Con un ejemplo simple, por supuesto, todo parece elemental. Pero con el crecimiento de la aplicación probada y el aumento en el número de pruebas, no quiero duplicar la búsqueda de elementos de IU antes de cada prueba. Además, deberá estructurar estos elementos de la interfaz de usuario, ya que puede haber muchas pantallas. Para hacer esto, haga que las pruebas de escritura sean más convenientes.


Objetos de pantalla


En Android ( Kakao ), este problema se resuelve agrupando elementos de IU de cada pantalla en Pantalla (Objeto de página). Aquí se puede aplicar un enfoque similar, con la excepción de que en Flutter, para realizar acciones con elementos de la interfaz de usuario, no solo necesita Finder (para buscar un elemento de la interfaz de usuario), sino también FlutterDriver (para realizar una acción), por lo que debe almacenar un enlace a FlutterDriver en Screen .


Para definir cada elemento de la interfaz de usuario, agregamos la clase DWidget (D - de la palabra Dart en este caso). Para crear un DWidget necesitará un FlutterDriver , con el que se realizarán acciones en este elemento de la interfaz de usuario, así como una ValueKey , que coincide con el ValueKey Flutter del widget de la aplicación con la que queremos interactuar:


 class DWidget { final FlutterDriver _driver; final SerializableFinder _finder; DWidget(this._driver, dynamic valueKey) : _finder = find.byValueKey(valueKey); ... 

find.byValueKey(…) al crear manualmente cada DWidget inconveniente, por lo tanto, es mejor pasar el valor de ValueKey al ValueKey , y el DWidget sí obtendrá el SerializableFinder deseado. Tampoco es muy conveniente pasar manualmente FlutterDriver al crear cada DWidget , por lo que puede almacenar FlutterDriver en BaseScreen y transferirlo a DWidget , y agregar un nuevo método para BaseScreen para crear BaseScreen :


 abstract class BaseScreen { final FlutterDriver _driver; BaseScreen(this._driver); DWidget dWidget(dynamic key) => DWidget(_driver, key); ... 

Por lo tanto, crear clases de pantallas y obtener elementos de la interfaz de usuario en ellas será mucho más fácil:


 class MainScreen extends BaseScreen { MainScreen(FlutterDriver driver) : super(driver); DWidget get button => dWidget('button'); DWidget get textField => dWidget('text_field'); ... } 

Deshacerse de await


Otra cosa no muy conveniente al escribir pruebas con FlutterDriver es la necesidad de agregar await antes de cada acción:


 await driver.tap(button); await driver.scrollUntilVisible(list, checkBox); await driver.tap(checkBox); await driver.tap(text); await driver.enterText("some text"); 

Olvidar la await es fácil, y sin ella, las pruebas no funcionarán correctamente, porque los métodos del driver devuelven Future<void> y cuando se llaman sin await ejecutan hasta la primera await dentro del método, y el resto del método se "pospone hasta más tarde".


Puede TestAction esto creando una TestAction que "envolverá" Future para que podamos esperar hasta que se complete una acción antes de pasar a la siguiente:


 typedef TestAction = Future<void> Function(); 

(esencialmente, TestAction es cualquier función (o lambda) que devuelve un Future<void> )


Ahora puede ejecutar fácilmente la secuencia TestAction sin TestAction innecesarias:


 Future<void> runTestActions(Iterable<TestAction> actions) async { for (final action in actions) { await action(); } } 

Usando TestAction en un DWidget


DWidget usa para interactuar con elementos de la interfaz de usuario, y será muy conveniente si estas acciones son TestAction para que puedan usarse en el método runTestAction . Para hacer esto, la clase DWidget tendrá métodos de acción:


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

Ahora puede escribir pruebas de la siguiente manera:


 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"), ]); 

Si necesita realizar alguna acción en runTestActions que no esté relacionada con DWidget , entonces solo necesita crear una lambda que devuelva un Future<void> :


 await runTestActions([ mainScreen.result.hasText("summa = 0"), () => driver.requestData("some_message"), () async => print("some_text"), mainScreen.field_1.setText("3"), ]); 

FlutterDriverHelper


FlutterDriver tiene varios métodos para interactuar con elementos de la interfaz de usuario (presionar, recibir e ingresar texto, desplazamiento, etc.) y para estos métodos DWidget tiene los métodos correspondientes que devuelven TestAction .


Por conveniencia, todo el código descrito en este artículo se publica como la biblioteca FlutterDriverHelper en pub.dev .


Para las listas de desplazamiento en las que los elementos se crean dinámicamente (por ejemplo, ListView.builder ), FlutterDriver tiene un 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 { ... } 

Este método desplaza el widget desplazable en la dirección especificada hasta que el widget del item aparece en la pantalla (o hasta que se timeout un timeout ). Para no pasar scrollable en cada desplazamiento, se agregó la clase DScrollItem, que hereda un DWidget y representa un elemento de la lista. Contiene un enlace a scrollable , por lo que al desplazarse solo queda para especificar dyScroll o 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 las pruebas, puede tomar capturas de pantalla de la aplicación, y FlutterDriverHelper tiene un Screenshoter que guarda las capturas de pantalla en la carpeta correcta con la hora actual y puede funcionar con TestAction .


Otros problemas y sus soluciones.


  • No pude encontrar una forma estándar de hacer clic en los botones en los cuadros de diálogo de hora / fecha: tengo que usar TestHooks . TestHooks también puede ser útil para cambiar la hora / fecha actual durante la prueba.
  • en la lista DropdownButtonFormField de DropdownButtonFormField debe especificar la key no para DropdownMenuItem , sino para el elemento child este DropdownMenuItem , de lo contrario, el Flutter Driver no podrá encontrarlo. Además, el desplazamiento en la lista desplegable aún no funciona ( problema en github.com ).
  • el método FlutterDriver.getCenter devuelve Future<DriverOffset> , pero DriverOffset no DriverOffset parte de la API pública ( Problema en github.com )
  • Hay algunas cosas más problemáticas y no obvias que ya existen. Puedes leer sobre ellos en un maravilloso artículo . Particularmente útil fue la capacidad de ejecutar pruebas en el escritorio y restablecer el estado de la aplicación antes del inicio de cada prueba.
  • Puede ejecutar pruebas usando las acciones de Github. Más detalles aquí .

Todo


Las FlutterDriverHelper futuras para FlutterDriverHelper incluyen:


  • desplazamiento automático al elemento de lista deseado si en el momento de acceder no está visible en la pantalla (como se hace en la biblioteca de Kaspresso para Android). Si es posible, incluso en ambas direcciones.
  • interceptores para acciones realizadas con un Dwidget o DscrollItem .

Comentarios y comentarios constructivos son bienvenidos.


Actualización (15/01/2020) : en la versión 1.1.0 TestAction convirtió en una clase, con el campo String name . Y gracias a esto, se runTestActions registro de todas las acciones realizadas en el método runTestActions .

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


All Articles