Flutter Arquitectura Limpia [11] – Implementación de Bloc1/2

Flutter Arquitectura Limpia [11] – Implementación de Bloc 1/2

 

El titular de la lógica de presentación que vamos a utilizar en la aplicación Number Trivia es BLoC. Ya hemos configurado sus eventos y estados en la parte anterior. Ahora llega el momento de comenzar a armar todo haciendo un desarrollo basado en pruebas con Dart’s Streams.

Debo aclarar que el contenido original es de Resocoder,  lo que he hecho  es una traducción al español del contenido. Al final de este artículo está el video en inglés para que vayan ilustrándose mejor y ya tengan una claridad de lo expuesto

Curso TDD Arquitectura Limpia

001 draw

Esta publicación es solo una parte de una serie de tutoriales. ¡Todas las demás partes las puede ver aquí y aprenda a diseñar sus aplicaciones Flutter!

Configuración 

Claro, tenemos las clases Event y State ya utilizables desde NumberTriviaBloc, pero también tenemos que pensar qué dependencias tendrá.

Dado que un Bloc (o cualquier otro titular de lógica de presentación) está en el límite entre el dominio y las capas de presentación, dependerá de los dos casos de uso que tengamos para nuestra aplicación. Luego, por supuesto, también usará el InputConverter creado en la parte anterior.

usecase to ploc diagram small

Creemos por primera vez el constructor junto con los campos primero. Vamos a hacer algunos trucos de constructor con verificación null y asignación de campo fuera de las llaves.

number_trivia_bloc.dart

class NumberTriviaBloc extends Bloc<NumberTriviaEvent, NumberTriviaState> {
  final GetConcreteNumberTrivia getConcreteNumberTrivia;
  final GetRandomNumberTrivia getRandomNumberTrivia;
  final InputConverter inputConverter;

  NumberTriviaBloc({
    // Changed the name of the constructor parameter (cannot use 'this.')
    @required GetConcreteNumberTrivia concrete,
    @required GetRandomNumberTrivia random,
    @required this.inputConverter,
    // Asserts are how you can make sure that a passed in argument is not null.
    // We omit this elsewhere for the sake of brevity.
  })  : assert(concrete != null),
        assert(random != null),
        assert(inputConverter != null),
        getConcreteNumberTrivia = concrete,
        getRandomNumberTrivia = random;

  @override
  NumberTriviaState get initialState => Empty();

  @override
  Stream<NumberTriviaState> mapEventToState(
    NumberTriviaEvent event,
  ) async* {
    // TODO: Add Logic
  }
}

El archivo de prueba, como de costumbre, vivirá debajo de una ubicación reflejada, lo que significa prueba / features / number_trivia / presentation / bloc. Vamos a configurarlo con todos los Mocks apropiados.

number_trivia_bloc_test.dart

class MockGetConcreteNumberTrivia extends Mock
    implements GetConcreteNumberTrivia{}

class MockGetRandomNumberTrivia extends Mock implements GetRandomNumberTrivia{}

class MockInputConverter extends Mock implements InputConverter{}

void main() {
  NumberTriviaBloc bloc;
  MockGetConcreteNumberTrivia mockGetConcreteNumberTrivia;
  MockGetRandomNumberTrivia mockGetRandomNumberTrivia;
  MockInputConverter mockInputConverter;

  setUp(() {
    mockGetConcreteNumberTrivia = MockGetConcreteNumberTrivia();
    mockGetRandomNumberTrivia = MockGetRandomNumberTrivia();
    mockInputConverter = MockInputConverter();

    bloc = NumberTriviaBloc(
      concrete: mockGetConcreteNumberTrivia,
      random: mockGetRandomNumberTrivia,
      inputConverter: mockInputConverter,
    );
  });
}

Estado Inicial

La primera prueba es bastante simple y, de hecho, ¡ya está implementada!  Así es, estamos rompiendo el principio TDD aquí debido al código generado por la extensión Bloc para VS Code.

test.dart

test('initialState should be Empty', () {
  // assert
  expect(bloc.initialState, equals(Empty()));
});

Y lo has adivinado, la propiedad initialState ya devuelve Empty ().

implementation.dart

@override
NumberTriviaState get initialState => Empty();

Pruebas Controladas por Eventos

001 lab

 

Toda la lógica del Bloque se ejecuta en el método mapEventToState (). Esto significa que para probar el Bloc, tenemos que imitar los widgets de la interfaz de usuario enviando los eventos apropiados directamente desde la prueba.

En esta parte, vamos a comenzar a probar con el evento GetTriviaForConcreteNumber, por lo que crearemos un grupo de prueba con el mismo nombre. También configuremos las variables con las que vamos a probar en este grupo.

 

test.dart

group('GetTriviaForConcreteNumber', () {
  // The event takes in a String
  final tNumberString = '1';
  // This is the successful output of the InputConverter
  final tNumberParsed = int.parse(tNumberString);
  // NumberTrivia instance is needed too, of course
  final tNumberTrivia = NumberTrivia(number: 1, text: 'test trivia');
}

Asegurando Validación y Conversión

Lo más importante que sucede cuando se despacha un GetTriviaForConcreteNumber es asegurarse de que el string obtenido de la UI sea un entero positivo válido. Gracias a la belleza de las dependencias, ya tenemos la lógica necesaria para esta validación y conversión: está dentro del InputConverter. Debido a esto, podemos adherirnos al principio de responsabilidad única, asumir que el InputConverter se implementa (con suerte) con éxito y el mock como de costumbre.

La primera prueba solo verificará que el método InputConverter haya sido invocado.

test.dart

test(
  'should call the InputConverter to validate and convert the string to an unsigned integer',
  () async {
    // arrange
    when(mockInputConverter.stringToUnsignedInteger(any))
        .thenReturn(Right(tNumberParsed));
    // act
    bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
    await untilCalled(mockInputConverter.stringToUnsignedInteger(any));
    // assert
    verify(mockInputConverter.stringToUnsignedInteger(tNumberString));
  },
);

Como de costumbre, ejecute la prueba y fallará. Vamos a lograrlo en el siguiente paso.

Hacemos await untilCalled () porque la lógica dentro de un Bloque se activa a través de un Stream <Event> que, por supuesto, es asíncrono. Si no hubiéramos esperado hasta que se haya llamado a stringToUnsignedInteger, la verificación siempre fallaría, ya que verificaríamos antes de que el código tuviera la oportunidad de ejecutarse.

implementation.dart

@override
Stream<NumberTriviaState> mapEventToState(
  NumberTriviaEvent event,
) async* {
  // Immediately branching the logic with type checking, in order
  // for the event to be smart casted
  if (event is GetTriviaForConcreteNumber) {
    inputConverter.stringToUnsignedInteger(event.numberString);
  }
}

Falla de entrada inválida

002 phone call

Si la conversión es exitosa, el código continuará obteniendo datos del caso de uso GetConcreteNumberTrivia, que se probará a fondo en las pruebas posteriores. Primero, sin embargo, tratemos con lo que sucede cuando falla la conversión. En ese caso, es responsabilidad de NumberTriviaBloc informar a la UI qué salió mal al emitir un estado de Error.

La clase Error necesita que se pase un mensaje de error. Vamos a saltar un poco hacia adelante y crear constantes para todos los mensajes, solo para que no pasemos cadenas mágicas desde el principio. Habrá un mensaje por cada Failure distinta que puede ocurrir dentro de las dependencias de NumberTriviaBloc. Pon este código al principio del archivo:

 

number_trivia_bloc.dart

const String SERVER_FAILURE_MESSAGE = 'Server Failure';
const String CACHE_FAILURE_MESSAGE = 'Cache Failure';
const String INVALID_INPUT_FAILURE_MESSAGE =
    'Invalid Input - The number must be a positive integer or zero.';

Para poner a prueba la lógica descrita anteriormente, vamos a utilizar una forma diferente de prueba, en comparación con lo que ya estamos acostumbrados, que es adecuado para Streams.

Hasta ahora, todos los métodos que probamos para un valor devolvieron el valor ellos mismos. Por ejemplo, llamar a InputConverter.stringToUnsignedInteger () devuelve Either <Failure, int>. Incluso los métodos que devuelven un Future son fáciles de manejar, solo aguarde y estará listo.

Con Bloc, se llama dispatch con un evento para ejecutar la lógica, pero el despacho en sí mismo devuelve nulo. Los valores reales se emiten desde un lugar completamente diferente, desde el flujo contenido dentro de un campo de estado del bloque. Sé que este párrafo es probablemente demasiado abstracto para comprenderlo por sí solo, todo se aclarará con una prueba:

test.dart

test(
  'should emit [Error] when the input is invalid',
  () async {
    // arrange
    when(mockInputConverter.stringToUnsignedInteger(any))
        .thenReturn(Left(InvalidInputFailure()));
    // assert later
    final expected = [
      // The initial state is always emitted first
      Empty(),
      Error(message: INVALID_INPUT_FAILURE_MESSAGE),
    ];
    expectLater(bloc.state, emitsInOrder(expected));
    // act
    bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
  },
);

Creamos una lista de Estados que esperamos que se emitan y luego establecemos un marco de prueba para decirle que en algún momento en el futuro (expectLater) el Stream debería emitir los valores de la Lista en un orden preciso con el comparador emitsInOrder. Luego llamamos a bloc.dispatch para comenzar.

En lugar de la disposición habitual -> actuar -> afirmar, en su lugar organizamos -> afirmar más tarde -> actuar. Por lo general, no es necesario llamar a expectLater antes de enviar el evento, ya que el Stream tarda un tiempo en emitir su primer valor. Sin embargo, me gusta equivocarme en el lado seguro.

Será en la siguiente implementación donde verá el verdadero poder de Either. Usando su método de plegado (fold), simplemente tenemos que manejar tanto el caso de falla como el de éxito y, a diferencia de las excepciones, no hay una forma simple de evitarlo.

Estamos usando la palabra clave yield* que significa rendimiento para poder anidar prácticamente un generador asíncrono (async *) dentro de otro método asíncrono *.

implementation.dart

@override
Stream<NumberTriviaState> mapEventToState(
  NumberTriviaEvent event,
) async* {
  if (event is GetTriviaForConcreteNumber) {
    final inputEither =
        inputConverter.stringToUnsignedInteger(event.numberString);

    yield* inputEither.fold(
      (failure) async* {
        yield Error(message: INVALID_INPUT_FAILURE_MESSAGE);
      },
      // Although the "success case" doesn't interest us with the current test,
      // we still have to handle it somehow. 
      (integer) => throw UnimplementedError(),
    );
  }
}

Aunque estamos lanzando un UnimplementedError desde el caso Right () que contiene el entero convertido, esto no causará ningún problema en las dos pruebas que tenemos actualmente.

 

A continuación

En esta parte comenzamos a implementar NumberTriviaBloc haciendo un desarrollo basado en pruebas con Streams. También hemos visto la razón para usar Either en acción. En la siguiente parte, terminaremos el Bloc, haciéndolo manejar eventos tanto concretos como aleatorios.

Aquí les comparto el video

Previous

Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

Flutter Arquitectura Limpia [12] – Implementación de Bloc 2/2

Next

Deja un comentario