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

22  comments

The presentation logic holder we're going to use in the Number Trivia App is BLoC. We've already set up its Events and States in the previous part. Now comes the time to start putting it all together doing test-driven development with Dart's Streams.

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!

Setup

Sure, we have the Event and State classes already usable from within the NumberTriviaBloc but we also have to think about which dependencies it's going to have.

Since a Bloc (or any other presentation logic holder) is at the boundary between the domain and presentation layers, it will depend on the two use cases we have for our app. Then, of course, it will also use the InputConverter created in the previous part.

Boundary between domain and presentation

Let's for once create the constructor together with the fields first. We're going to do some constructor trickery with null checking and field assignment outside of the curly braces.

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

The test file will, as usual, live under a mirrored location, which means test / features / number_trivia / presentation / bloc.  Let's set it up with all the appropriate Mocks. ​​

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,
    );
  });
}
If you're wondering about the imports, there's quite a lot of them. Check out the GitHub repo for the full project together with imports.

Initial State

The first test is rather simple and actually, it's already implemented! ? That's right, we're breaking the TDD principle here because of the code generated by the Bloc extension for VS Code.​​

test.dart

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

And you guessed it, the property initialState already returns Empty().

implementation.dart

@override
NumberTriviaState get initialState => Empty();

Event-Driven Testing

All of the Bloc's logic is executed in the mapEventToState() method. This means that to test the Bloc, we have to mimic the UI widgets by dispatching appropriate Events right from the test.

In this part, we're going to start testing with the GetTriviaForConcreteNumber event, so we'll create a test group with the same name. Let's also set up the variables we're going to test with in this group.

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

Ensuring Validation & Conversion

The most important thing to happen when a GetTriviaForConcreteNumber is dispatched, is to make sure the String gotten from the UI is a valid positive integer. Through the beauty of dependencies, we already have the logic needed for this validation and conversion - it's inside the InputConverter. Because of this, we can adhere to the single responsibility principle, assume that the InputConverter is (hopefully ?) successfully implemented and mock it as usual.

The first test will just verify that the InputConverter's method has in fact been called.

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

As usual, run the test and it will fail. We're going to make it in the next step.

We await untilCalled() because the logic inside a Bloc is triggered through a Stream<Event> which is, of course, asynchronous itself. Had we not awaited until the stringToUnsignedInteger has been called, the verification would always fail, since we'd verify before the code had a chance to execute.​​

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

Invalid Input Failure

If the conversion is successful, the code will continue with getting data from the GetConcreteNumberTrivia use case, which will be thoroughly tested in subsequent tests. First, however, let's deal with what happens when the conversion fails. In that case, it's the NumberTriviaBloc's responsibility to let the UI know what went wrong by emitting an Error state.

The Error class needs an error message to be passed in. We're going to skip ahead a bit and create constants for all the messages, just so that we won't be passing around magical strings right from the start. There will be one message per a distinct Failure which can occur inside the NumberTriviaBloc's dependencies. Put this code at the beginning of the file:

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

To put the logic described above into a test, we're going to use a different way of testing, compared to what we're already used to, which is suitable for Streams.

Up until now, all the methods we tested for a value returned the value themselves. For example, calling InputConverter.stringToUnsignedInteger() returns Either<Failure, int>. Even methods which return a Future are easy to deal with - just await it and you're set.

With Bloc, you call dispatch with an Event to execute the logic, but dispatch itself returns void. The actual values are emitted from a completely different place - from the  Stream contained inside a state field of the Bloc. I know this paragraph is probably too abstract to comprehend on its own, everything will become clearer with a test:

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

We create a list of States which we expect to be emitted and then set a tell the testing framework that sometime in the future (expectLater) the Stream should emit the values from the List in a precise order with the emitsInOrder matcher. Then we call bloc.dispatch to kick things off.

Instead of the usual arrange -> act -> assert, we instead arrange -> assert later -> act. It is usually not be necessary to call expectLater before actually dispatching the event because it takes some time before a Stream emits its first value. I like to err on the safe side though.

It will be in the following implementation where you will see the true power of Either. Using its fold method, we simply have to handle both the failure and the success case and unlike with exceptions, there is no simple way around it.​​

We're using the yield* keyword meaning yield each to be able to practically nest an async generator (async*) within another async* method.

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

Although we are throwing an UnimplementedError from the Right() case which contains the converted integer, this won't cause any trouble in the two tests we currently have.

Coming Up Next

In this part we started implementing the NumberTriviaBloc doing test-driven development with Streams. We've also seen the reason for using Either in action. In the next part, we'll finish the Bloc, making it handle both concrete and random events.

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
  • Hi Matt. As always, thanks for the great information.
    I am implementing these patterns in my current Flutter app, and I have a question:

    One feature of my app is basically a Google Calendar clone. So, when the user taps the screen, I want to create a “temporary” time segment, so that the user can move it around, change its duration, or delete it.
    So, when they create this temporary segment, I do not want to necessarily store it in SharedPreferences, because it’s fine if this data gets lost.

    So, where should I put temporary data that I don’t need to save locally? I don’t want to put it in the Bloc because then I will be putting logic in there.
    I was thinking I could make a third data source and call it InMemoryDataSource, and use the repository to store it there. I could use it like a Redux Store.
    Do you think that would be a good approach, or should I just save it in the Bloc?

    Thank you in advance!

    • Real-life solutions require sacrifices. You can surely create a new Data Source together with a bunch of Use Cases, but that adds a lot of code which may or may not be desired.
      I’d just do it directly in the Bloc, at least in the beginning and then see whether separating it out makes sense.

      • Awesome. Yeah I think that is the best solution for now.
        Either way the approach seems pretty clean, since the Bloc receives the touch input and then yields the new state, so it’s really not a lot of logic.
        Thanks again.

  • I actually really like this methodology of development. This eases the pressure of choosing a different strategy for every app and would reduce the number of instances where we end up using a certain solution such that the app won’t scale any more. This will be a major break from a lot of headaches.

    Also, I would like to thank Matej Rešetár for the contribution he is providing and the benefits of such a great contribution to the community.

  • Thx for great tutorial!
    In BLoC version v1.0.0 instead of ”expectLater(bloc.state…”
    should be “expectLater(bloc” because bloc object implements Stream by themself.
    If it possible add point about version of bloc should be used.
    I’m think it may help some peoples.)

  • Hi Matt! Bloc library is updated. I changed block.dispatch to block.add and can’t pass test… Output message is:
    ERROR: Expected: should do the following in order:
    • emit an event that Empty:
    • emit an event that Error:
    Actual: Empty:
    Which: was not a Stream or a StreamQueue

    • Hello! As Ivan Lobanov writes: “In BLoC version v1.0.0 instead of ”expectLater(bloc.state…”
      should be “expectLater(bloc” because bloc object implements Stream by themself.
      If it possible add point about version of bloc should be used.
      I’m think it may help some peoples.)”

  • I as getting the same error

    ERROR: Expected: should do the following in order:
    • emit an event that Empty:
    • emit an event that Error:

    even without using “bloc.state”.

    I just added a “*” here

    test(
    ‘should emit [Error] when the input is invalid’,
    () async * {

    then the test succeeded.

  • “Although we are throwing an UnimplementedError from the Right() case which contains the converted integer, this won’t cause any trouble in the two tests we currently have.” For me is return a error => Unhandled error UnimplementedError occurred in Instance of ‘NumberTriviaBloc’.

  • Please i have been on the error for a while can you help me out

    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:

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