Skip to content

300 Testing

Fasust edited this page Nov 25, 2019 · 44 revisions

Page Table of Contents

Introduction

Testing has become an essential part of developing any large scale application and there is strong evidence that writing tests leads to a higher code quality [78]. This chapter aims to give you a brief introduction to how testing in Flutter [1] works and more specifically, how to test an app that implements the BLoC Pattern [7].

Types of Tests in Flutter

Flutters official test library [79] differentiates between three types of tests:

Unit Tests

Unit Tests can be run very quickly. They can test any function of your app, that does not require the rendering of a Widget [80]. Their main use-case is to test business logic or in our case: BLoCs [7].

Widget Tests

Widget Tests are used to test small Widget Sub-Trees of your application. They run relatively quickly and can test the behavior of a given UI [80].

Integration Test (Driver Tests)

Integration/Driver Tests run your entire application in a virtual machine or on a physical device. They can test user-journeys and complete use-cases. They are very slow and “prone to braking”[80].

Flutter test comparison

Figure 24: Flutter test comparison [81]

Writing Unit Tests

I will focus on Unit Tests for this guide. The Flutter Team recommends that the majority of Flutter tests should be Unit Test [80], [81]. The fact that they are quick to write and quick to execute makes up for their relatively low confidence. In addition to this, because we are using the BLoC Pattern, our UI shouldn’t contain that much testable code anyways. Or to paraphrase the BLoC pattern creator: We keep our UI so stupid we don’t need to test it [7]. First, we have to import the test library [79] and the mockito package [82] in our pubspec.yaml:

dev_dependencies:
  mockito: ^4.1.1
  flutter_test:
    sdk: flutter

Code Snippet 37: Pubspec.yaml Test Imports

flutter_test offers the core testing capabilities of Flutter. mockito is used to mock up dependencies. All our tests should sit in a directory named “test” on the root level of our app directory. If we want to place them somewhere else, we have to specify their location every time we want to run them.

Wisgen test directory

Figure 25: Wisgen test directory [11]

All test files have to end with the postfix "_test.dart" to be recognized by the framework [80].

Now we can start writing our tests. For this example, I will test the favorite BLoC of Wisgen [11]:

void main() {

  ///Related test are grouped together 
  ///to get a more readable output 
  group('Favorite Bloc', () {
    FavoriteBloc favoriteBloc;

    setUp((){
      //Run before each test
      favoriteBloc = new FavoriteBloc();
    });

    tearDown((){
      //Run after each test
      favoriteBloc.dispose();
    });
    
    test('Initial State is an empty list', () {
      expect(favoriteBloc.currentState, List());
    });

    ...
  });
}

Code Snippet 38: Wisgen Favorite BLoC Tests 1 [11]

We can use the group() function to group related tests together. This way the output of our tests is more neatly formated [80]. setUp() is called once before every test, so it is perfect for initializing our BLoC [83]. tearDown() is called after every test, so we can use it to dispose of our BLoC. The test() function takes in a name and a callback with the actual test. In our test, we check if the State of the favorite BloC after initialization is an empty list. expect() takes in the actual value and the value that is expected: expect(actual, matcher). We can run all our tests using the command flutter test.

Testing Streams

Now a more relevant topic when working with the BLoC Pattern, the testing of Streams [40]:

void main() {

  group('Favorite Bloc', () {
    FavoriteBloc favoriteBloc;

    setUp((){...}); //Snippet 38

    tearDown((){...}); //Snippet 38
    
    
    test('Initial State is an empty list', () {...}); //Snippet 38

    test('Stream many Events and see if the State is emitted in the correct order', () {
      //Set Up
      Wisdom wisdom1 = Wisdom(id: 1, text: "Back up your pictures", type: "tech");
      Wisdom wisdom2 = Wisdom(id: 2, text: "Wash your ears", type: "Mum's Advice");
      Wisdom wisdom3 = Wisdom(id: 3, text: "Travel while you're young", type: "Grandma's Advice");

      //Testing
      favoriteBloc.dispatch(FavoriteEventAdd(wisdom1));
      favoriteBloc.dispatch(FavoriteEventAdd(wisdom2));
      favoriteBloc.dispatch(FavoriteEventRemove(wisdom1));
      favoriteBloc.dispatch(FavoriteEventAdd(wisdom3));

      //Result
      expect( 
          favoriteBloc.state,
          emitsInOrder([
            List(), //BLoC Library BLoCs emit their initial State on creation.
            List()..add(wisdom1),
            List()..add(wisdom1)..add(wisdom2),
            List()..add(wisdom2),
            List()..add(wisdom2)..add(wisdom3)
          ]));
    });
  });
}

Code Snippet 39: Wisgen Favorite BLoC Tests 2 [11]

In this test, we create three wisdoms and add/remove them from the favorite BLoC by sending the corresponding Events. We use the emitsInOrder() matcher to tell the framework that we are working with a Stream and looking for a specific set of Events to be emitted in order [83]. The Flutters test framework also offers many other Stream matchers besides emitsInOrder() [84]:

  • emits() matches a single data Event.
  • emitsError() matches a single error Event.
  • emitsDone matches a single done Event.
  • emitsAnyOf() consumes events matching one (or more) of several possible matchers.
  • emitsInAnyOrder() works like emitsInOrder(), but it allows the matchers to match in any order.
  • neverEmits() matches a Stream that finishes without matching an inner matcher.
  • And more [84]

Mockito

As mentioned before, Mockito [82] can be used to mock dependencies. The BLoC Pattern forces us to make all platform-specific dependencies of our BLoCs injectable [7]. This comes in very handy when testing BLoCs. For example, the wisdom BLoC of Wisgen fetches data from a given Repository. Instead of testing the Wisdom BLoC in combination with its Repository, we can inject a mock Repository into the BLoC. This way we can test one bit of logic at a time. In this example, we use Mockito to test if our wisdom BLoC emits new wisdoms after receiving a fetch event:

//Creating Mocks using Mockito
class MockRepository extends Mock implements Supplier<Wisdom> {}
class MockBuildContext extends Mock implements BuildContext {}

void main() {
  group('Wisdom Bloc', () {
    WisdomBloc wisdomBloc;
    MockRepository mockRepository;
    MockBuildContext mockBuildContext;

    setUp(() {
      wisdomBloc = WisdomBloc();
      mockRepository = MockRepository();
      mockBuildContext = MockBuildContext();
      //Inject Mock
      wisdomBloc.repository = mockRepository;
    });

    tearDown(() {
      //Run after each test
      wisdomBloc.dispose();
    });

    test('Send Fetch Event and see if it emits correct wisdom', () {
      //Set Up ---
      List<Wisdom> fetchedWisdom = [
        Wisdom(id: 1, text: "Back up your Pictures", type: "tech"),
        Wisdom(id: 2, text: "Wash your ears", type: "Mum's Advice"),
        Wisdom(id: 3, text: "Travel while you're young", type: "Grandma's Advice")
      ];
      
      //Telling the Mock Repo how to behave
      when(mockRepository.fetch(20, mockBuildContext))
        .thenAnswer((_) async => fetchedWisdom);

      List expectedStates = [
        IdleWisdomState(new List()), //BLoC Library BLoCs emit their initial State on creation
        IdleWisdomState(fetchedWisdom)
      ];
    
      //Testing ---
      wisdomBloc.dispatch(FetchEvent(mockBuildContext));

      //Result ---
      expect(wisdomBloc.state, emitsInOrder(expectedStates));
    });
  });
}

Code Snippet 40: Wisgen Wisdom BLoC Tests with Mockito [11]

First, we create our Mock classes. For this test, we need a mock Supplier-Repository and a mock BuildContext [34]. In the setUp() function, we initialize our BLoC and our mocks and inject the mock Repository into our BLoC. In the test() function, we tell our mock Repository to send a list of three wisdoms when its fetch() function is called. Now we can send a fetch event to the BLoC, and check if it emits the correct states in order.

Equality in Dart

By default, all comparisons in Dart work based on references and not base on values [83], [85]
Wisdom wisdom1 = Wisdom(id: 1, text: "Back up your Pictures", type: "tech");
Wisdom wisdom2 = Wisdom(id: 1, text: "Back up your Pictures", type: "tech");

print(wisdom1 == wisdom2); //false

Code Snippet 41: Equality in Flutter

This can be an easy thing to trip over during testing, especially when comparing States emitted by BLoCs. Luckily, Felix Angelov released the Equatable package in 2019 [85]. It’s an easy way to overwrite how class equality is handled. If we make a class extend the Equatable class, we can set the properties it is compared by. We do this by overwriting its props attribute. This is used in Wisgen to make the States of the wisdom BLoC compare based on the wisdom they carry:

@immutable
abstract class WisdomState extends Equatable {}

///Broadcasted from [WisdomBloc] on network error.
class WisdomStateError extends WisdomState {
  final Exception exception;
  WisdomStateError(this.exception);

  @override
  List<Object> get props => [exception]; //compare based on exception.
}

///Gives access to current list of [Wisdom]s in the [WisdomBloc].
///
///When the BLoC receives a [WisdomEventFetch] during this State, 
///it fetches more [Wisdom] from it [Supplier]. 
///When done it emits a new [IdleSate] with more [Wisdom].
class WisdomStateIdle extends WisdomState {
  final List<Wisdom> wisdoms;
  WisdomStateIdle(this.wisdoms);

  @override
  List<Object> get props => wisdoms; //compare based on wisdoms.
}

Code Snippet 42: Wisgen Wisdom States with Equatable [11]

If we wouldn’t use Equatable, the test form snippet 40 could not functions properly, as two states carrying the same wisdom would still be considered different by the test framework.

🕐 TLDR If you don’t want your classes to be compared base on their reference, use the Equatable package [85]

Next Chapter: Conventions >

Back to Top