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

[wp_ad_camp_1]

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

 

Comenzamos a implementar NumberTriviaBloc en la parte anterior y aprendiste los conceptos básicos para hacer TDD con Streams. En esta parte, terminemos la implementación de Bloc para que luego podamos pasar a la inyección de dependencia.
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!

 

Índice

    Continuando donde lo dejamos

    001 ui 2

     

    Ya implementamos una parte importante de la lógica que se ejecuta cada vez que el evento GetTriviaForConcreteNumber llega a la conversión de entrada Bloc. Sin embargo, después de tener el int convertido y validado con éxito para el que el usuario quiere ver algunas curiosidades numéricas sorprendentes, actualmente solo lanzamos una excepción no implementada. ¡Cambiemos eso!

     

     

    [wp_ad_camp_2]

    Probando con un caso de uso

    Lo primero que debe suceder es que el caso de uso GetConcreteNumberTrivia se ejecute con un argumento de número adecuado. Por ejemplo, si numberString del evento es "42", el entero pasado al caso de uso debería ser, por supuesto, 42.

    Tenga en cuenta que ahora tenemos que hacer mock del caso de uso InputConverter y GetConcreteNumberTrivia en la parte de organización de la prueba.

     

    number_trivia_bloc_test.dart

    test(
      'should get data from the concrete use case',
      () async {
        // arrange
        when(mockInputConverter.stringToUnsignedInteger(any))
            .thenReturn(Right(tNumberParsed));
        when(mockGetConcreteNumberTrivia(any))
            .thenAnswer((_) async => Right(tNumberTrivia));
        // act
        bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
        await untilCalled(mockGetConcreteNumberTrivia(any));
        // assert
        verify(mockGetConcreteNumberTrivia(Params(number: tNumberParsed)));
      },
    );

    Nuevamente implementaremos el código suficiente para que la prueba pase. Podemos ignorar con seguridad todas las advertencias en el IDE por ahora diciendo que el método no devuelve un Stream.

    number_trivia_bloc.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);
          },
          (integer) {getConcreteNumberTrivia(Params(number: integer));},
        );
      }
    }

    Refabricando las Pruebas

    [wp_ad_camp_3]

    Haciendo mocking al InputConverter para retornar un valor satisfactorio,

    when(mockInputConverter.stringToUnsignedInteger(any))
        .thenReturn(Right(tNumberParsed));

    ocurrirá en casi todas las pruebas. Es una buena idea ponerlo en su propio método setUpMockInputConverterSuccess y usarlo en todas las pruebas, incluida la de la parte anterior.

     

    number_trivia_bloc_test.dart

    group('GetTriviaForConcreteNumber', () {
      ...
    
      void setUpMockInputConverterSuccess() =>
          when(mockInputConverter.stringToUnsignedInteger(any))
              .thenReturn(Right(tNumberParsed));
      // Use in tests below
      ...
    }

    Ejecución exitosa

    002 checkmark

    Por supuesto, simplemente ejecutar el caso de uso no es suficiente. Necesitamos que la UI sepa lo que está sucediendo para que pueda mostrarle algo al usuario. ¿Qué estados debe emitir el Bloc cuando el caso de uso se ejecuta sin problemas?

    Te puede interesar:  Flutter Arquitectura Limpia [9] – Fuente de Datos Remota

    Bueno, antes de llamar al caso de uso, es una buena idea mostrar algunos comentarios visuales al usuario. Si bien mostrar un Indicador de progreso circular es una tarea para la UI, debemos notificar a la UI para que lo muestre emitiendo un estado de carga.

    Después de que el caso de uso devuelva el lado derecho de Cualquiera de los dos (lo que significa éxito), el Bloc debe pasar el NumberTrivia obtenido a la UI dentro de un estado Cargado.

    [wp_ad_camp_4]

    test.dart

    test(
      'should emit [Loading, Loaded] when data is gotten successfully',
      () async {
        // arrange
        setUpMockInputConverterSuccess();
        when(mockGetConcreteNumberTrivia(any))
            .thenAnswer((_) async => Right(tNumberTrivia));
        // assert later
        final expected = [
          Empty(),
          Loading(),
          Loaded(trivia: tNumberTrivia),
        ];
        expectLater(bloc.state, emitsInOrder(expected));
        // act
        bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
      },
    );

    La implementación comenzará a ponerse fea con todo el anidamiento, pero, por supuesto, la refactorizaremos después de terminar de escribir las pruebas.

    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);
          },
          (integer) async* {
            yield Loading();
            final failureOrTrivia = await getConcreteNumberTrivia(
              Params(number: integer),
            );
            yield failureOrTrivia.fold(
              (failure) => throw UnimplementedError(),
              (trivia) => Loaded(trivia: trivia),
            );
          },
        );
      }
    }

    Ejecución fallida

    El tipo Either nos hace manejar el caso Failure, pero a partir de ahora, simplemente estamos lanzando un UnimplementedError cuando esto sucede. Creemos una prueba para el caso cuando el caso de uso (y, en consecuencia, el repositorio y así sucesivamente en la cadena) devuelva un error de servidor.

    test.dart

    test(
      'should emit [Loading, Error] when getting data fails',
      () async {
        // arrange
        setUpMockInputConverterSuccess();
        when(mockGetConcreteNumberTrivia(any))
            .thenAnswer((_) async => Left(ServerFailure()));
        // assert later
        final expected = [
          Empty(),
          Loading(),
          Error(message: SERVER_FAILURE_MESSAGE),
        ];
        expectLater(bloc.state, emitsInOrder(expected));
        // act
        bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
      },
    );

    implementation.dart

    (integer) async* {
      yield Loading();
      final failureOrTrivia = await getConcreteNumberTrivia(
        Params(number: integer),
      );
      yield failureOrTrivia.fold(
        (failure) => Error(message: SERVER_FAILURE_MESSAGE),
        (trivia) => Loaded(trivia: trivia),
      );
    },

    Simple, ¿no es así? Sin embargo, ServerFailure no es el único tipo de falla devuelta por el caso de uso. También se puede producir un CacheFailure y tenemos que manejarlo.

    Es muy importante manejar todos los subtipos posibles de Falla dentro de un Bloc. Después de todo, queremos mostrar un mensaje de error significativo al usuario y eso es posible solo emitiendo estados de error con un mensaje personalizado.

    [wp_ad_camp_5]

    La prueba de CacheFailure será muy similar a la anterior, pero lo suficientemente diferente como para que la refactorización no sea realmente necesaria.

    test.dart

    test(
      'should emit [Loading, Error] with a proper message for the error when getting data fails',
      () async {
        // arrange
        setUpMockInputConverterSuccess();
        when(mockGetConcreteNumberTrivia(any))
            .thenAnswer((_) async => Left(CacheFailure()));
        // assert later
        final expected = [
          Empty(),
          Loading(),
          Error(message: CACHE_FAILURE_MESSAGE),
        ];
        expectLater(bloc.state, emitsInOrder(expected));
        // act
        bloc.dispatch(GetTriviaForConcreteNumber(tNumberString));
      },
    );

    Estamos a punto de llegar a un código de aspecto realmente desagradable. Sin embargo, no se preocupe, ya que lo vamos a refactorizar en un momento.

    Te puede interesar:  Flutter y la creación de aplicaciones móvil y web

     

    implementation.dart

    (integer) async* {
      yield Loading();
      final failureOrTrivia = await getConcreteNumberTrivia(
        Params(number: integer),
      );
      yield failureOrTrivia.fold(
        (failure) => Error(
          message: failure is ServerFailure
              ? SERVER_FAILURE_MESSAGE
              : CACHE_FAILURE_MESSAGE,
        ),
        (trivia) => Loaded(trivia: trivia),
      );
    },

    Refactorizando el Operador Ternario

     

    003 art and design

    La forma en que decidimos el mensaje del estado de Error deja mucho que desear. El operador ternario es en realidad una opción insegura: ¿qué pasa si hay algún otro subtipo de falla del que no tenemos conocimiento? Quiero decir, no debería suceder con Clean Architecture, pero aún así ... No importa el tipo real de la falla, a menos que sea una falla de servidor, ahora siempre tendrá asignada una CACHE_FAILURE_MESSAGE.

    Creemos un método auxiliar _mapFailureToMessage separado en el que vamos a tratar de manejar los mensajes de manera más adecuada. Todavía estamos dentro de los límites de Vanilla Dart, que no admite clases selladas fuera de la caja, por lo que todavía tenemos que manejar el tipo de Failure inesperado (por si acaso).

    Todo el método mapEventToState ahora se ve así:

    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);
          },
          (integer) async* {
            yield Loading();
            final failureOrTrivia = await getConcreteNumberTrivia(
              Params(number: integer),
            );
            yield failureOrTrivia.fold(
              (failure) => Error(message: _mapFailureToMessage(failure)),
              (trivia) => Loaded(trivia: trivia),
            );
          },
        );
      }
    }
    
    String _mapFailureToMessage(Failure failure) {
      // Instead of a regular 'if (failure is ServerFailure)...'
      switch (failure.runtimeType) {
        case ServerFailure:
          return SERVER_FAILURE_MESSAGE;
        case CacheFailure:
          return CACHE_FAILURE_MESSAGE;
        default:
          return 'Unexpected Error';
      }
    }

    Obteniendo el Random Trivia

    004 gambling

    Como ya estamos acostumbrados de las partes anteriores, vamos a romper intencionalmente los principios de TDD para no volvernos locos con la implementación tediosa de básicamente el mismo código por segunda vez. A diferencia de las fuentes de datos o el repositorio, tanto el código de prueba como el código de producción para GetTriviaForRandomNumber serán más simples. ¡Esto se debe a que no usa InputValidator! Después de todo, no se necesita ningún número ingresado por el usuario para obtener curiosidades de números aleatorios.

    Comenzando con las pruebas, vamos a copiar y pegar el código del grupo concreto, eliminar todo lo relacionado con el InputConverter y también cambiar cualquier referencia al caso o evento de uso concreto para hacer referencia al caso o evento de uso aleatorio.

     

    test.dart

    group('GetTriviaForRandomNumber', () {
      final tNumberTrivia = NumberTrivia(number: 1, text: 'test trivia');
    
      test(
        'should get data from the random use case',
        () async {
          // arrange
          when(mockGetRandomNumberTrivia(any))
              .thenAnswer((_) async => Right(tNumberTrivia));
          // act
          bloc.dispatch(GetTriviaForRandomNumber());
          await untilCalled(mockGetRandomNumberTrivia(any));
          // assert
          verify(mockGetRandomNumberTrivia(NoParams()));
        },
      );
    
      test(
        'should emit [Loading, Loaded] when data is gotten successfully',
        () async {
          // arrange
          when(mockGetRandomNumberTrivia(any))
              .thenAnswer((_) async => Right(tNumberTrivia));
          // assert later
          final expected = [
            Empty(),
            Loading(),
            Loaded(trivia: tNumberTrivia),
          ];
          expectLater(bloc.state, emitsInOrder(expected));
          // act
          bloc.dispatch(GetTriviaForRandomNumber());
        },
      );
    
      test(
        'should emit [Loading, Error] when getting data fails',
        () async {
          // arrange
          when(mockGetRandomNumberTrivia(any))
              .thenAnswer((_) async => Left(ServerFailure()));
          // assert later
          final expected = [
            Empty(),
            Loading(),
            Error(message: SERVER_FAILURE_MESSAGE),
          ];
          expectLater(bloc.state, emitsInOrder(expected));
          // act
          bloc.dispatch(GetTriviaForRandomNumber());
        },
      );
    
      test(
        'should emit [Loading, Error] with a proper message for the error when getting data fails',
        () async {
          // arrange
          when(mockGetRandomNumberTrivia(any))
              .thenAnswer((_) async => Left(CacheFailure()));
          // assert later
          final expected = [
            Empty(),
            Loading(),
            Error(message: CACHE_FAILURE_MESSAGE),
          ];
          expectLater(bloc.state, emitsInOrder(expected));
          // act
          bloc.dispatch(GetTriviaForRandomNumber());
        },
      );
    });

    Con todas estas pruebas fallando por razones obvias, vamos a copiar la implementación para cuando GetTriviaForRandomNumber llegue al Bloc desde la contraparte concreta. Por supuesto, no manejamos ninguna conversión de entrada aquí, por lo que vamos a copiar solo el código que trata con el entero ya convertido.

    Te puede interesar:  Flutter Arquitectura Limpia [10] – Bloc Scaffolding y Conversión de Entrada

    Solo hay dos cambios para hacer en este código copiado: llame al caso de uso GetRandomNumberTrivia y pase NoParams () ya que el caso de uso, bueno ..., no acepta ningún parámetro.

    implementation.dart

    @override
    Stream<NumberTriviaState> mapEventToState(
      NumberTriviaEvent event,
    ) async* {
      if (event is GetTriviaForConcreteNumber) {
        ...
      } else if (event is GetTriviaForRandomNumber) {
        yield Loading();
        final failureOrTrivia = await getRandomNumberTrivia(
          NoParams(),
        );
        yield failureOrTrivia.fold(
          (failure) => Error(message: _mapFailureToMessage(failure)),
          (trivia) => Loaded(trivia: trivia),
        );
      }
    }

    Después de que este código de implementación haya pasado todas las pruebas, es hora de (redoble de batería, por favor ) ...

    Remover los Duplicados

    En realidad, no hay mucha duplicación para eliminar, pero aún así, podemos hacer que nuestro código sea un poco más DRY extrayendo la emisión de los estados Error y Loaded.

     

    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);
          },
          (integer) async* {
            yield Loading();
            final failureOrTrivia = await getConcreteNumberTrivia(
              Params(number: integer),
            );
            yield* _eitherLoadedOrErrorState(failureOrTrivia);
          },
        );
      } else if (event is GetTriviaForRandomNumber) {
        yield Loading();
        final failureOrTrivia = await getRandomNumberTrivia(
          NoParams(),
        );
        yield* _eitherLoadedOrErrorState(failureOrTrivia);
      }
    }
    
    Stream<NumberTriviaState> _eitherLoadedOrErrorState(
      Either<Failure, NumberTrivia> either,
    ) async* {
      yield either.fold(
        (failure) => Error(message: _mapFailureToMessage(failure)),
        (trivia) => Loaded(trivia: trivia),
      );
    }
    
    String _mapFailureToMessage(Failure failure) {
      switch (failure.runtimeType) {
        case ServerFailure:
          return SERVER_FAILURE_MESSAGE;
        case CacheFailure:
          return CACHE_FAILURE_MESSAGE;
        default:
          return 'Unexpected Error';
      }
    }

    Este tipo de refactorización nos insta a volver a ejecutar las pruebas una vez más y, por supuesto, todas pasan. Y con esta refactorización, acabamos de terminar otra parte de nuestra aplicación Number Trivia: el titular de la lógica de presentación  en forma de BLoC.

     

    ¿Qué sigue?

    Nos estamos acercando a la línea de meta. Ahora que hemos implementado literalmente toda la lógica, es hora de unir todas las partes con la inyección de dependencia y luego ... ¡Ya no nos quedará nada más que crear que la interfaz de usuario!

    Aquí les comparto el video

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

    Subir