The presentation logic holder we're going to use in the Number Trivia App is BLoC. We've already set up its Event
s and State
s in the previous part. Now comes the time to start putting it all together doing test-driven development with Dart's Stream
s.
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.
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 Mock
s.
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,
);
});
}
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 Event
s 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.
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 Stream
s.
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 State
s 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.
expectLater
before actually dispatch
ing 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 Stream
s. 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.
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.
You are welcome!
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.)
Thanks!!! It helped me 😉
Thanks, I should have seen the comment first. Great Job.
thank you
I think he modified the article, anyway thank you.
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.)”
use “bloc” instead of “bloc.state”
thanks man
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:
I have the same error. did you manage to solve?
If is there anyone interested in these tutorial codes for new flutter versions, here is the code: https://github.com/luizpaulofranz/flutter-clean-architecture, this is totally developed with flutter 2.5.1, I hope it helps. And Matt, thanks for sharing your knowledge!
Thanks so much! I got through about here in the course making changes as needed for current packages, etc. But was pretty stuck on the Bloc constructor – you saved me hours!
Thanks Luiz
thanks a lot Luiz