Bloc Test Tutorial – Easier Way to Test Blocs in Dart & Flutter

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 Streams.

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 Streams 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.

All of the tests are passing

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()]),
  );
});
Matej Rešetár
 

Matej is an app developer with a knack for teaching others. If he's not programming, making tutorials or doing other business, he's mostly working out, listening to audiobooks and taking cold showers.

>