We started to implement the NumberTriviaBloc
in the previous part and you learned the basics of doing TDD with Stream
s. In this part, let's finish the Bloc implementation so that we can move on to dependency injection next.
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.
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!
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.
You the real MVP!
Thanks!
Thanks a lot for this learning material but I think part eleven has been omitted.
In case anyone is looking for part 11 (1/2) here is the link: https://resocoder.com/2019/10/10/flutter-tdd-clean-architecture-course-11-bloc-implementation/
I would like to take the opportunity to thank Matej for the high quality of these course notes, as well as for the videos: they smoothly guided me into the adoption of TDD and Clean Architecture for my future dart/flutter apps
Thank you! The “missing” 11th part is now fixed and available among all the other parts.
[…] 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á […]
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));
},
);
is this method working for the later videos
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 got this this error too
I encounter this too. but the test was successfully passed
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?
actually is emitting loading and loaded (there was mistake in my code) but Empty() state never emits which is initialstate. why initial state is not emitting? What I am doing wrong.
This might help you, buddy
https://github.com/felangel/bloc/issues/1518
Here’s how I’ve done it:
final expected = [
Loading(),
Loaded(tNumberTrivia),
];
expect(numberTriviaBloc.state, equals(Empty()));
expectLater(numberTriviaBloc, emitsInOrder(expected));
Have a look at this:
https://github.com/felangel/bloc/issues/1518#issuecomment-663014717
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!!
It is a callable class not an abstract one, it delegates getting the information to repository
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());
},
);
});
}
Thanks for sharing. I read many of your blog posts, cool, your blog is very good. https://www.binance.com/en-NG/register?ref=JHQQKNKN