Blocs are meant to hold logic and, as proper programmers, we should probably write some tests to verify whether or not the logic is correct. If you follow test-driven development, writing tests is even more crucial. There's one problem though - testing Blocs requires a lot of boilerplate.
The bloc_test package sets out to solve this issue by providing abstractions even for testing! No more dealing with classic Stream
s.
Starter project
The starter project comes from the Flutter Bloc v1.0.0 tutorial and it went through some very minor changes to comply with the version 2.1.1. The app fetches fake weather forecasts and displays them. We also have a concept of just weather and also detailed weather - represented as two separate events, GetWeather
and GetDetailedWeather
.
To showcase the new bloc_test package, we will tackle testing only the logic which runs upon receiving the GetWeather
event.
weather_bloc.dart
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
final WeatherRepository weatherRepository;
WeatherBloc(this.weatherRepository);
@override
WeatherState get initialState => WeatherInitial();
@override
Stream<WeatherState> mapEventToState(
WeatherEvent event,
) async* {
yield WeatherLoading();
if (event is GetWeather) {
try {
final weather = await weatherRepository.fetchWeather(event.cityName);
yield WeatherLoaded(weather);
} on NetworkError {
yield WeatherError("Couldn't fetch weather. Is the device online?");
}
} else if (event is GetDetailedWeather) {
// Not interested in this...
}
}
}
As you can see from the code above, when GetWeather
event is received, the bloc outputs WeatherLoading
and then either WeatherLoaded
or WeatherError
.
Adding dependencies
There's just one dependency we need to add to the starter project and that's bloc_test. We will want to use mockito as well, but that already comes together with bloc_test.
pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^2.1.1
equatable: ^1.0.1
dev_dependencies:
flutter_test:
sdk: flutter
bloc_test: ^2.2.2
Testing
In the olden days, you'd test Blocs just like regular Stream
s using stream matchers such as emitsInOrder
. This comes with a lot of boilerplate.
weather_bloc_test.dart
class MockWeatherRepository extends Mock implements WeatherRepository {}
void main() {
MockWeatherRepository mockWeatherRepository;
setUp(() {
mockWeatherRepository = MockWeatherRepository();
});
group('GetWeather', () {
final weather = Weather(cityName: 'London', temperatureCelsius: 7);
// Old way of testing Blocs - like regular Streams
test(
'OLD WAY emits [WeatherLoading, WeatherLoaded] when successful',
() {
when(mockWeatherRepository.fetchWeather(any))
.thenAnswer((_) async => weather);
final bloc = WeatherBloc(mockWeatherRepository);
bloc.add(GetWeather('London'));
expectLater(
bloc,
emitsInOrder([
WeatherInitial(),
WeatherLoading(),
WeatherLoaded(weather),
]),
);
},
);
});
}
emitsExactly ☝
The bloc_test package adds a new Bloc-only matcher. While it's nothing spectacular, it indeed reduces the amount of boilerplate by not having to write expectLater
.
weather_bloc_test.dart
test(
'NEWER WAY BUT LONG-WINDED emits [WeatherLoading, WeatherLoaded] when successful',
() {
when(mockWeatherRepository.fetchWeather(any))
.thenAnswer((_) async => weather);
final bloc = WeatherBloc(mockWeatherRepository);
bloc.add(GetWeather('London'));
emitsExactly(bloc, [
WeatherInitial(),
WeatherLoading(),
WeatherLoaded(weather),
]);
},
);
blocTest ?
Now we're finally entering an exciting territory. The blocTest
method is an all-in-one solution for testing Blocs, encapsulating their creation, adding events and expecting states.
weather_bloc_test.dart
blocTest(
'emits [WeatherLoading, WeatherLoaded] when successful',
build: () {
when(mockWeatherRepository.fetchWeather(any))
.thenAnswer((_) async => weather);
return WeatherBloc(mockWeatherRepository);
},
act: (bloc) => bloc.add(GetWeather('London')),
expect: [WeatherInitial(), WeatherLoading(), WeatherLoaded(weather)],
);
The previous three test methods tested the exact same thing - the states outputted when everything goes according to plan. To finish testing the whole GetWeather
event, let's add one last test implemented with blocTest
for then there's a NetworkError
.
weather_bloc_test.dart
blocTest(
'emits [WeatherLoading, WeatherError] when unsuccessful',
build: () {
when(mockWeatherRepository.fetchWeather(any)).thenThrow(NetworkError());
return WeatherBloc(mockWeatherRepository);
},
act: (bloc) => bloc.add(GetWeather('London')),
expect: [
WeatherInitial(),
WeatherLoading(),
WeatherError("Couldn't fetch weather. Is the device online?"),
],
);
Upon running these tests, they are all going to pass.
Mocking a Bloc
Unit testing a Bloc which depends on another Bloc is also something which would benefit from some minor boilerplate removal. That's precisely why the bloc_test library comes with a nifty MockBloc
! While the weather forecast app has only one Bloc, let's still showcase how you can mock it. Inside a new test file:
weather_bloc_test.dart
class MockWeatherBloc extends MockBloc<WeatherEvent, WeatherState>
implements WeatherBloc {}
void main() {
MockWeatherBloc mockWeatherBloc;
setUp(() {
mockWeatherBloc = MockWeatherBloc();
});
//TODO: Add test
}
Now, mocking the outputted states is as simple as providing a Stream<State>
to the whenListen
method. You'll most likely want to construct the Stream
from an Iterable
.
weather_bloc_test.dart
test('Example mocked BLoC test', () {
whenListen(
mockWeatherBloc,
Stream.fromIterable([WeatherInitial(), WeatherLoading()]),
);
expectLater(
mockWeatherBloc,
emitsInOrder([WeatherInitial(), WeatherLoading()]),
);
});
How it is working without extending equitable?
Your article helped me a lot, is there any more related content? Thanks!