Flutter TDD Clean Architecture Course [12] – Bloc Implementation 2/2

24  comments

We started to implement the NumberTriviaBloc in the previous part and you learned the basics of doing TDD with Streams. In this part, let's finish the Bloc implementation so that we can move on to dependency injection next.

TDD Clean Architecture Course
This post is just one part of a tutorial series. See all of the other parts here and learn to architect your Flutter apps!

Continuing Where We Left Off

We already implemented an important part of the logic which runs whenever the GetTriviaForConcreteNumber event arrives in the Bloc - input conversion. However, after we have the successfully converted and validated int for which the user wants to see some amazing number trivia, we currently just throw an UnimplementedException. Let's change that!

Testing with a Use Case

The first thing that should happen is that the GetConcreteNumberTrivia use case should be executed with a proper number argument. For example, if the event's numberString is "42", the integer passed into the use case should of course be 42.

Notice that we now have to mock both the InputConverter and the GetConcreteNumberTrivia use case in the arrange part of the test.​​

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

We're again going to implement just enough code for the test to pass. We can safely ignore all the warnings in the IDE for now saying that the method doesn't return a 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));
      },
    );
  }
}

Refactoring the Tests

Mocking the InputConverter to return a successful value

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

will happen in almost every test. It's a good idea to put it into its own method setUpMockInputConverterSuccess and use that throughout all the tests, including the one from the previous part.

number_trivia_bloc_test.dart

group('GetTriviaForConcreteNumber', () {
  ...

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

Successful Execution

Of course, just simply executing the use case is not enough. We need to let the UI know what's going on so that it can display something to the user. Which states should the Bloc emit when the use case executes smoothly?

Well, before calling the use case, it's a good idea to show some visual feedback to the user. While displaying a CircularProgressIndicator is a task for the UI, we have to notify the UI to show it by emitting a Loading state.

After the use case returns the Right side of Either (which means success), the Bloc should pass the obtained NumberTrivia to the UI inside a Loaded state.

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

The implementation will start to get ugly with all the nesting, but we're of course going to refactor it after we're done writing tests.

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

Unsuccessful Execution

The Either type makes us handle the Failure case, but as of now, we're simply throwing an UnimplementedError when this happens. Let's create a test for the case when the use case (and consequently the repository and so on down the chain) returns a ServerFailure.

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, isn't it? ServerFailure is not the only kind of Failure returned by the use case though. A CacheFailure can also occur and we have to handle that.

It's very important to handle all the possible subtypes of Failure inside a Bloc. After all, we want to show a meaningful error message to the user and that's possible only by emitting Error states with a customized message.

Test for the CacheFailure will be very similar to the previous one, yet different enough that refactoring is not really needed.

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

We're about to get to some really nasty looking code. Don't worry though as we're going to refactor it in just a little while.

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

Refactoring the Ternary Operator

The way we decide on the message of the Error state leaves a lot to be desired. The ternary operator is actually an unsafe choice - what if there's some other Failure subtype which we're not aware of. I mean, it shouldn't happen with Clean Architecture, but still... No matter the actual type of the Failure, unless it's a ServerFailure, it's now always going to have a CACHE_FAILURE_MESSAGE assigned to it.

Let's create a separate _mapFailureToMessage helper method where we're going to try to handle the messages more appropriately. We're still within the confines of vanilla Dart which doesn't support  sealed classes out of the box, so we still need to handle the unexpected Failure type (just in case).

The whole mapEventToState method now looks like this:

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';
  }
}

Getting the Random Trivia

As we're already used to from the previous parts, we're going to intentionally break TDD principles so that we don't drive ourselves crazy with tediously implementing basically the same code for the second time. Unlike with the data sources or the repository, both the test code and the production code for the GetTriviaForRandomNumber will be simpler. That's because it doesn't use the InputValidator! No user-inputted number is needed to get random number trivia, after all.

Starting with the tests, we're going to copy and paste the code from the concrete group, remove everything dealing with the InputConverter and also change any references to the concrete use case or event to reference the random use case or event.

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

With all of these tests failing for obvious reasons, we're going to copy the implementation for when the GetTriviaForRandomNumber arrives at the Bloc from the concrete counterpart. Of course, we don't handle any input conversion here, so we're going to copy only the code which deals with the already-converted integer.​​

There are only two changes to make in this copied code - call the GetRandomNumberTrivia use case and pass in NoParams() since the use case, well ..., doesn't accept any parameters ?

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

After this implementation code made all of the tests pass, it's time for (drumroll please ?)...

Removing the Duplication

There's actually not that much duplication to remove, but still, we can make our code a bit more DRY by extracting the emitting of the Error and Loaded states.

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';
  }
}

This kind of a refactoring urges us to rerun the tests once again and, of course, they still all pass. And with this refactoring, we've just finished another part of our Number Trivia App - the presentation logic holder (should I say PLoC ?)​​ in the form of a BLoC.​​

Coming Up Next

We're getting close to the finish line. Now that we have literally every bit of logic implemented, it's time to put all the parts together with dependency injection and then... There will be nothing more left for us to do than to create the UI!

Icons and other attribution GOES HERE

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter Developer at LeanCode and a developer educator, he is set on helping other people succeed in their Flutter app development career.

You may also like

Flutter UI Testing with Patrol

Flutter UI Testing with Patrol
  • Thanks so much for taking the time to put this series together! It’s a lot of info, but you will see the beauty of Clean architecture.

    To anyone going through this, and if you’ve downloaded the sample app, after you update flutter_bloc and equatable you’ll have to replace calls to bloc.dispatch() with bloc.add(). I did this but the bloc tests failed, saying they were expecting a Stream but didn’t get one. When I changed
    expectLater(bloc.state, emitsInOrder(expected) with
    expectLater(bloc, emitsInOrder(expected) it worked. Since the bloc is a Stream, and emitsInOrder() is checking a Stream, not a State.
    Now all tests pass, in the sample and in the project I’m converting to Clean.

  • If I have an application with: authentication, registration, settings, categories, sub categories, classifieds for categories. What are the features should I have.?

  • ON NEW UPDATE put async* on the second function as well this should remove the test errors

    class NumberTriviaBloc…

    yield* inputEither.fold(
    (failure) async* {
    yield Error(message: INVALID_INPUT_FAILURE_MESSAGE);
    },
    (integer) async* {
    getConcreteNumberTrivia(Params(number: integer));
    },
    );

  • So I tried to run the test on GetConcreteNumberTrivia and got some really wried errors.
    Apparently, if you are running the test in
    flutter_bloc: ^4.0.1 or even ^4.0.0

    you will get the following error :

    Testing started at 6:05 PM …
    /Users/jimmy/Desktop/Flutter/flutter/bin/flutter –no-color test –machine –plain-name GetTriviaForConcreteNumber test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart
    package:bloc/src/bloc.dart 146:7 Bloc.onError.
    package:bloc/src/bloc.dart 147:6 Bloc.onError
    ===== asynchronous gap ===========================
    dart:async _StreamImpl.listen
    package:bloc/src/bloc.dart 260:7 Bloc._bindEventsToStates
    package:bloc/src/bloc.dart 59:5 new Bloc
    package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart new NumberTriviaBloc
    test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart 30:12 main.
    This test failed after it had already completed. Make sure to use [expectAsync]
    or the [completes] matcher when testing async code.
    package:bloc/src/bloc.dart 146:7 Bloc.onError.
    package:bloc/src/bloc.dart 147:6 Bloc.onError
    ===== asynchronous gap ===========================
    dart:async _StreamImpl.listen
    package:bloc/src/bloc.dart 260:7 Bloc._bindEventsToStates
    package:bloc/src/bloc.dart 59:5 new Bloc
    package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart new NumberTriviaBloc
    test/features/number_trivia/presentation/bloc/number_trivia_bloc_test.dart 30:12 main.

    Unhandled error NoSuchMethodError: The method ‘fold’ was called on null.
    Receiver: null
    Tried calling: fold(Closure: (Failure) => Error, Closure: (NumberTrivia) => Loaded) occurred in bloc Instance of ‘NumberTriviaBloc’.
    #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:53:5)
    #1 NumberTriviaBloc._eitherLoadedOrErrorState (package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart:68:27)

    #2 NumberTriviaBloc.mapEventToState. (package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart:55:18)

    #3 NumberTriviaBloc.mapEventToState. (package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart)
    #4 Right.fold (package:dartz/src/either.dart:92:64)
    #5 NumberTriviaBloc.mapEventToState (package:clean_architecture_tdd_course/features/number_trivia/presentation/bloc/number_trivia_bloc.dart:46:26)

    #6 Bloc._bindEventsToStates. (package:bloc/src/bloc.dart:252:20)
    #7 Stream.asyncExpand.onListen. (dart:async/stream.dart:579:30)
    #8 StackZoneSpecification._registerUnaryCallback.. (package:stack_trace/src/stack_zone_specification.dart:129:26)
    #9 StackZoneSpecification._run (package:stack_trace/src/stack_zone_specification.dart:209:15)
    #10 StackZoneSpecification._registerUnaryCallback. (package:stack_trace/src/stack_zone_specification.dart:129:14)
    #11 _rootRunUnary (dart:async/zone.dart:1192:38)
    #12 _CustomZone.runUnary (dart:async/zone.dart:1085:19)
    #13 _CustomZone.runUnaryGuarded (dart:async/zone.dart:987:7)
    #14 _BufferingStreamSubscription._sendData (dart:async/stream_impl.dart:339:11)
    #15 _DelayedData.perform (dart:async/stream_impl.dart:594:14)
    #16 _StreamImplEvents.handleNext (dart:async/stream_impl.dart:710:11)
    #17 _PendingEvents.schedule. (dart:async/stream_impl.dart:670:7)
    #18 StackZoneSpecification._run (package:stack_trace/src/stack_zone_specification.dart:209:15)
    #19 StackZoneSpecification._registerCallback. (package:stack_trace/src/stack_zone_specification.dart:119:48)
    #20 _rootRun (dart:async/zone.dart:1180:38)
    #21 _CustomZone.run (dart:async/zone.dart:1077:19)
    #22 _CustomZone.runGuarded (dart:async/zone.dart:979:7)
    #23 _CustomZone.bindCallbackGuarded. (dart:async/zone.dart:1019:23)
    #24 StackZoneSpecification._run (package:stack_trace/src/stack_zone_specification.dart:209:15)
    #25 StackZoneSpecification._registerCallback. (package:stack_trace/src/stack_zone_specification.dart:119:48)
    #26 _rootRun (dart:async/zone.dart:1184:13)
    #27 _CustomZone.run (dart:async/zone.dart:1077:19)
    #28 _CustomZone.runGuarded (dart:async/zone.dart:979:7)
    #29 _CustomZone.bindCallbackGuarded. (dart:async/zone.dart:1019:23)
    #30 _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
    #31 _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)

    After some trial and error, I found that by adding :

    when(mockGetConcreteNumberTrivia(any)).thenAnswer((_) async => Right(tNumberTrivia));

    inside the test of ‘should call the InputConverter to validate and convert the string to an unsigned integer’
    works:

    test(
    ‘should call the InputConverter to validate and convert the string to an unsigned integer’,
    () async {
    // arrange
    setUpMockInputConverterSuccess();
    when(mockGetConcreteNumberTrivia(any)).thenAnswer((_) async => Right(tNumberTrivia));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    await untilCalled(mockInputConverter.stringToUnsignedInteger(any));
    // assert
    verify(mockInputConverter.stringToUnsignedInteger(tNumberString));
    },
    );

    not sure if that is the right solution, but it works.

  • I am using latest bloc library and there is minor change in bloc.
    My NumberTriviaBloc constructor is
    NumberTriviaBloc({
    @required GetConcreteNumberTrivia concrete,
    @required GetRandomNumberTrivia random,
    @required this.inputConverter,
    }) : assert(concrete != null),
    assert(random != null),
    assert(inputConverter != null),
    getConcreteNumberTrivia = concrete,
    getRandomNumberTrivia = random,
    super(Empty());

    and test.dart is
    //assert later
    final expected = [
    Empty(),
    Loading(),
    Loaded(trivia: tNumberTrivia),
    ];
    expectLater(bloc, emitsInOrder(expected));

    but the bloc does not emits empty and loading state.
    the error is
    GetTriviaForConcreteNumber should emit [Error] when the inout is invalid:

    ERROR: Expected: should do the following in order:
    • emit an event that Empty:
    • emit an event that Error:
    Actual:
    Which: emitted • Error
    which didn’t emit an event that Empty:

    what should I do?

  • Anyone facing the issue that test always passes irrespective of the expected states,
    using flutter_bloc: ^6.0.0

    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(CACHE_FAILURE_MESSAGE),
    ];
    expectLater(bloc.state, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    },
    );

    • turns out async* is not needed for test function.
      correct signature ->
      test(
      ‘should emit [Loading, Error] with a proper message for the error when getting data fails’,
      () async {
      ///
      }

  • This tutorial is mind-blowing. I can’t even learn it and this guy created it. Watched this whole tutorial for the second time and still have so much ambiguity. How I am gonna use this on a big project by myself!

  • final GetConcreteNumberTrivia getConcreteNumberTrivia;


    final failureOrTrivia = await getConcreteNumberTrivia(
    Params(number: integer),
    );

    I am not getting why we called the GetConcreteNumberTrivia Usecase over here? It just contains the declaration of methods. The definitions are in repository of Data layer. I am totally confused here, Please help!!

  • this worked for me

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

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

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

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

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

    void setUpMockInputConverterSuccess() => when(() => mockInputConverter.stringToUnsignedInteger(tNumberString))
    .thenReturn(Right(tNumberParsed));

    test(
    ‘should call the InputConverter to validate and convert the string to an unsigned integer’,
    () async* {
    // arrange
    setUpMockInputConverterSuccess();

    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    await untilCalled(()=> mockInputConverter.stringToUnsignedInteger(any()));
    // assert
    verify(() => mockInputConverter.stringToUnsignedInteger(tNumberString));
    },
    );

    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, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    },
    );

    test(
    ‘should get data from the concrete use case’,
    () async {
    // arrange
    setUpMockInputConverterSuccess();
    when(() => mockGetConcreteNumberTrivia(Params(number: tNumberParsed)))
    .thenAnswer((_) async => const Right(tNumberTrivia));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    await untilCalled(() => mockGetConcreteNumberTrivia(Params(number: tNumberParsed)));
    // assert
    verify(() => mockGetConcreteNumberTrivia(Params(number: tNumberParsed)));
    },
    );

    test(
    ‘should emit [Loading, Loaded] when data is gotten successfully’,
    () async* {
    // arrange
    setUpMockInputConverterSuccess();
    when(() => mockGetConcreteNumberTrivia(Params(number: tNumberParsed)))
    .thenAnswer((_) async => const Right(tNumberTrivia));
    // assert later
    final expected = [
    Empty(),
    Loading(),
    Loaded(trivia: tNumberTrivia),
    ];
    expectLater(bloc.stream, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    },
    );

    test(
    ‘should emit [Loading, Error] when getting data fails’,
    () async* {
    // arrange
    setUpMockInputConverterSuccess();
    when(() => mockGetConcreteNumberTrivia(Params(number: tNumberParsed)))
    .thenAnswer((_) async => Left(ServerFailure()));
    // assert later
    final expected = [
    Empty(),
    Loading(),
    Error(message: SERVER_FAILURE_MESSAGE),
    ];
    expectLater(bloc.stream, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForConcreteNumber(tNumberString));
    },
    );

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

    group(‘GetTriviaForRandomNumber’, () {
    // NumberTrivia instance is needed too, of course
    const tNumberTrivia = NumberTrivia(number: 1, text: ‘test trivia’);

    test(
    ‘should get data from the random use case’,
    () async {
    // arrange
    when(() => mockGetRandomNumberTrivia(NoParams()))
    .thenAnswer((_) async => const Right(tNumberTrivia));
    // act
    bloc.add(GetTriviaForRandomNumber());
    await untilCalled(() => mockGetRandomNumberTrivia(NoParams()));
    // assert
    verify(() => mockGetRandomNumberTrivia(NoParams()));
    },
    );

    test(
    ‘should emit [Loading, Loaded] when data is gotten successfully’,
    () async* {
    // arrange
    when(() => mockGetRandomNumberTrivia(NoParams()))
    .thenAnswer((_) async => const Right(tNumberTrivia));
    // assert later
    final expected = [
    Empty(),
    Loading(),
    Loaded(trivia: tNumberTrivia),
    ];
    expectLater(bloc, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForRandomNumber());
    },
    );

    test(
    ‘should emit [Loading, Error] when getting data fails’,
    () async* {
    // arrange
    when(() => mockGetRandomNumberTrivia(NoParams()))
    .thenAnswer((_) async => Left(ServerFailure()));
    // assert later
    final expected = [
    Empty(),
    Loading(),
    Error(message: SERVER_FAILURE_MESSAGE),
    ];
    expectLater(bloc, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForRandomNumber());
    },
    );

    test(
    ‘should emit [Loading, Error] with a proper message for the error when getting data fails’,
    () async* {
    // arrange
    when(() => mockGetRandomNumberTrivia(NoParams()))
    .thenAnswer((_) async => Left(CacheFailure()));
    // assert later
    final expected = [
    Empty(),
    Loading(),
    Error(message: CACHE_FAILURE_MESSAGE),
    ];
    expectLater(bloc, emitsInOrder(expected));
    // act
    bloc.add(GetTriviaForRandomNumber());
    },
    );
    });
    }

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >