-
Notifications
You must be signed in to change notification settings - Fork 37
300 Testing
Page Table of Contents
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].
Flutters official test library [79] differentiates between three types of 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 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/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].
Figure 24: Flutter test comparison [81]
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.
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
.
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]
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.
⚠ | 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] |
---|
This Guide is licensed under the Creative Commons License (Attribution-NoDerivatives 4.0 International)
Author: Sebastian Faust.