Giter Club home page Giter Club logo

flutter-bdd's Introduction

Flutter BDD

En este artículo vamos a crear una aplicación extremadamente simple para android devices pero siguiendo la estrategia de diseño guiado por el comportamiento Behavior Driven Development

Tomaremos la aplicación por defecto que nos proporciona flutter (push counter) modificando su comportamiento para que el contador consuma un servicio de conteo centralizado. Por simplicidad solo vamos a trabajar en la capa cliente de la aplicación (sin entrar en detalles de la construcción del servicio).

Algunas aclaraciones

  • En el ejemplo iremos tomando algunas decisiones que por motivos de practicidad no aclaramos el fundamento detrás de tal decisión. En tal caso cualquier comentario, pregunta o recomendación será muy bien bienvenida 😁

Prerrequisitos

Creamos la aplicacion por defecto

  • Creamos una nueva app image. O desde la terminal podemos ejecutar
mkdir flutter-bdd
cd flutter-bdd
flutter create --template=app --platforms android --project-name flutter_bdd --org com.example .
  • Seleccionamos un emulador android y ejecutamos la app image
flutter emulators
flutter emulators --launch Pixel_4_API_30
flutter run

Preparación del entorno BDD

flutter pub add bdd_widget_test --dev
  • Agregamos el paquete build_runner como dependencia de desarrollo
flutter pub add build_runner --dev
  • Eliminamos el test por defecto test/widget_test.dart

Nuestro primer escenario

En BDD se abordan los casos de uso de la aplicación o interacciones del usuario con la aplicación con ejemplos que se denominan escenarios. Las características y escenarios se describen mediante el uso de un lenguaje natural de especificación denominado Gherkin

A continuación vamos a crear la característica "Contador" con el escenario inicial de la aplicación.

test/counter.feature:

Feature: Counter
    
    Scenario: Show initial counter value
        Given the app is running
        Given counter value is {10}
        Then I see {10} value

En lenguaje coloquial estamos diciendo que cuando ejecutemos la aplicación y el servicio contador tenga un valor 10 entonces mostraremos este valor en la aplicación.

Ahora que tenemos especificada la primer iteración de nuestra aplicación. Vamos a generar el test de integración asociado que nos "guíe" en la construcción.

  • Generamos el test (y pasos) asociados a nuestro primer escenario mediante la ejecución del siguiente comando
flutter pub run build_runner build --delete-conflicting-outputs

Lo anterior nos genera un test de widget test/counter_test.dart con los pasos asociados a la ejecución del test. Los pasos de la ejecución del test son generados en la carpeta test/step/

TDD RED: Implementación de los pasos

Given the app is running

test/step/the_app_is_running.dart

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bdd/main.dart';

Future<void> theAppIsRunning(WidgetTester tester) async {
  await tester.pumpWidget(const MyApp());
}

Given counter value is {10}

test/step/counter_value_is.dart

import 'package:flutter_test/flutter_test.dart';

Future<void> counterValueIs(WidgetTester tester, int counterValue) async {}

Then I see {10} value

test/step/i_see_value.dart

import 'package:flutter_test/flutter_test.dart';

Future<void> iSeeValue(WidgetTester tester, int counterValue) async {
  await tester.pump();
  expect(find.text(counterValue.toString()), findsOneWidget);
}

Ejecutamos el test

flutter test

Veremos que el test falla porque no encuentra el valor '10'

00:02 +0: Counter Show initial counter value                                                                           
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
  Actual: _TextFinder:<zero widgets with text "10" (ignoring offstage widgets)>
   Which: means none were found but one was expected

TDD GREEN: Hacemos la implementación más simple

Modificamos el valor inicial de _counter en el estado del widget MyHomePage

lib/main.dart

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 10;

Y veremos que ahora el test pasa

TDD BLUE: Mejoras en el código

1- Separación de componentes

  • Vamos a separar los tres componentes principales cada uno en su archivo

image.

2- Eliminamos la lógica asociada al estado por defecto

lib/my_home_page.dart

import 'package:flutter/material.dart';

class _MyHomePageState extends State<MyHomePage> {
  final int _counter = 10;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

3- MVVM

Como paso siguiente vamos a eliminar el código duro del valor inicial 10 en nuestro HomeState. Para ello necesitaremos de un colaborador que nos indique cúal es el valor inicial del contador.

La comunicación entre vistas y modelo la vamos a efectuar siguiendo el patrón MVVM, motivo por el que vamos a crear un view model asociado a la vista.

Para poder acceder a la instancia del view model vamos a utilizar IoC (Inversión de control) mediante el paquete qinject

flutter pub add qinject

lib/my_home_page.dart

import 'package:flutter/material.dart';
import 'package:flutter_bdd/my_home_page_view_model.dart';
import 'package:provider/provider.dart';

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late MyHomePageViewModel _viewModel;

  @override
  void initState() {
    _viewModel = Provider.of<MyHomePageViewModel>(context);
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${_viewModel.counterValue}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

lib/my_home_page_view_model.dart

class MyHomePageViewModel {
  int get counterValue => 0;
}

Segundo escenario

En BDD vamos identificando necesidades del usuario de la aplicación, automatizamos este escenario y luego construímos la lógica asociada utilizando TDD. Esto puede ser visto como dos ciclos de desarrollo bien definidos como se muestra en la siguiente imagen:

En nuestro ejemplo luego de haber implementado el primer escenario deberíamos continuar implementando el view model, servicios, repositorios, servidor, etc.

Por simplicidad solo nos vamos a limitar a mostrar el ciclo de BDD y vamos a dejar de lado la implementación end to end (de extremo a extremo) de la aplicación.

Incluso vale aclarar que en general la técnica BDD se utiliza con test de aceptación (pruebas sobre la aplicación real) y en nuestro caso lo hemos aplicado con test unitarios de widgets.

Dicho lo anterior vamos a continuar con nuestro segundo escenario de uso lo cual nos permitirá seguir identificando necesidades de nuestra aplicación.

test/counter.feature

Feature: Counter
    
    Scenario: Show initial counter value
        Given counter value is {10}
        Given the app is running        
        Then I see {10} value

    Scenario: Tap add button
        Given counter value is {5}
        When tap add button
        Then I see {6} value
flutter pub run build_runner build --delete-conflicting-outputs
flutter test

test/step/tap_add_button.dart

En el paso "tap add button" tendremos que verificar que se informe al view model que se ha tapeado el botón de incremento. Simular el valor de retorno del valor del contador (incrementado en 1). Por último la vista deberá ser informada de que el valor del contador ha cambiado para poder redibujarse.

import 'package:flutter_bdd/my_home_page_view_model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:qinject/qinject.dart';

Future<void> tapAddButton(WidgetTester tester) async {
  await tester.tap(find.byTooltip('Increment'));

  var mockViewModel = Qinject.use<dynamic, MyHomePageViewModel>();
  verify(mockViewModel.onAddButtonTapped()).called(1);

  mockViewModel.counterValue.value = mockViewModel.counterValue.value + 1;
  mockViewModel.counterValue.notifyListeners();

  await tester.pumpAndSettle();
}

Para poder efectuar lo anterior tendremos que modificar "counterValue" en el view model para que pueda informar sus cambios de valores a la vista.

import 'package:flutter/foundation.dart';

class MyHomePageViewModel {
  ValueNotifier<int> get counterValue => ValueNotifier(0);

  onAddButtonTapped() {}
}

Finalmente modificamos la vista para poder responder a los cambios de valor en el contador e informar al view model cuando se presione el botón "Increment"

class _MyHomePageState extends State<MyHomePage> {
  late MyHomePageViewModel _viewModel;

  @override
  void initState() {
    _viewModel = qinjector.use();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            _counterValue(context),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _viewModel.onAddButtonTapped,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _counterValue(BuildContext context) {
    return ValueListenableBuilder<int>(
      valueListenable: _viewModel.counterValue,
      builder: (context, value, _) => Text(
        '$value',
        style: Theme.of(context).textTheme.headline4,
      ),
    );
  }
}

Comentarios finales

flutter-bdd's People

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.