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