Testing BLoC in Flutter
aseem wangoo
Posted on February 27, 2021
Website: https://flatteredwithflutter.com/testing-bloc-in-flutte/
We will cover briefly about
- Convention for Tests
- Mocking Github API
- Writing tests for BLoC
1. Convention for Tests
In general,
- test files should reside inside a
test
folder located at the root of your Flutter application or package.
- Test files should always end with
_test.dart
. - Tests are placed under the main function
void main() {
// ALL YOUR TESTS ARE PLACED HERE
}
- If you have several tests that are related to one another, combine them using the
group
function.
void main() {group('YOUR GROUP OF TESTS', () { test('TEST 1', () { });
test('TEST 2', () {
});
} }
- You can use a terminal to run the tests by
flutter test test/'YOUR TEST FILE NAME'.dart
2. Mocking Github API
Introducing Mockito (a mock library for Dart).
How does it work?
Let’s say we have a class like this
// Real classclass Cat { String sound() => "Meow"; Future<void> chew() async => print("Chewing...");}
We create a mock class out of our above class by
// Mock classclass MockCat extends Mock implements Cat {}
and create a mock object
// Create mock object.var cat = MockCat();
Integrate our Github API class
Github has a public endpoint exposed for searching the repositories, and we append the user-defined search term to it.
https://api.github.com/search/repositories?q='YOUR SEARCH TERM'
We have an implementation class (GithubApi) that includes the method for calling the above API.
class GithubApi implements GithubSearchContract {}
and our search function looks like this
where we call the API, fetch the results, and convert them into the SearchResult model.
Time to mock!!
class MockGithubSearchImpl extends Mock implements GithubApi {}
- As we saw above, we mock our GithubApi, by creating a class
MockGithubSearchImpl
which extendsMock
- Setup the mock classes inside the test file
SearchBloc searchBloc; MockGithubSearchImpl mockGithubSearch; setUp(() { mockGithubSearch = MockGithubSearchImpl(); searchBloc = SearchBloc(mockGithubSearch); }); tearDown(() { searchBloc?.dispose(); });
- setUp: Registers a function to be run before tests.
We register our bloc with the mock implementation of our API.
- tearDown: Registers a function to be run after tests.
We dispose of our bloc inside the tearDown.
Define Matchers for States
We defined 5 UI states as
class SearchNoTerm extends SearchState {
SearchNoTerm() : super(state: States.noTerm);
}
class SearchError extends SearchState {
SearchError() : super(state: States.error);
}
class SearchLoading extends SearchState {
SearchLoading() : super(state: States.loading);
}
class SearchPopulated extends SearchState {
final SearchResult result;
SearchPopulated(this.result) : super(state: States.populated);
}
class SearchEmpty extends SearchState {
SearchEmpty() : super(state: States.empty);
}
These states will be sent to UI, as per the BLoC logic.
- To ease the testing, we define typematchers, which basically creates a matcher instance of type [T].
const noTerm = TypeMatcher<SearchNoTerm>();
const loading = TypeMatcher<SearchLoading>();
const empty = TypeMatcher<SearchEmpty>();
const populated = TypeMatcher<SearchPopulated>();
const error = TypeMatcher<SearchError>();
3. Writing tests for BLoC
We instantiate a new instance SearchBloc
inside our setUp
to ensure that each test is run under the same conditions.
Also, we dispose of the bloc instance inside our tearDown
SearchBloc searchBloc;
MockGithubSearchImpl mockGithubSearch;
setUp(() {
mockGithubSearch = MockGithubSearchImpl();
searchBloc = SearchBloc(mockGithubSearch);
});
tearDown(() {
searchBloc?.dispose();
});
- setUp: Registers a function to be run before tests. This function will be called before each test is run.
- tearDown: Registers a function to be run after tests. This function will be called after each test is run.
Let the testing begin
- Check for null
test('throws AssertionError if contract is null', () {
expect(
() => SearchBloc(null),
throwsA(isAssertionError),
);
})
- We pass in a null value to our SearchBloc
- The response expected is an AssertionError
2. Check for the initial state
test('initial state should be NoTerm', () async {
await expectLater(searchBloc.state, emitsInOrder([noTerm]));
});
- In our SearchBloc, the initial state is set to noTerm (see above for typeMatcher)
- expectLater: Just like expect, but returns a Future that completes when the matcher has finished matching.
-
emitsInOrder: Returns a StreamMatcher that matches the stream if each matcher in
matchers
matches, one after another.
3. Check for empty term
test('hardcoded empty term', () async {
expect(searchBloc.state, emitsInOrder([noTerm, empty]));
searchBloc.onTextChanged.add('');
})
- We add an empty string into our onTextChanged sink
- The expected states are noTerm and then empty
4. Check for results from API
test('api returns results', () async {
final term = 'aseem';
when(searchBloc.api.search(term)).thenAnswer((_) async {
return SearchResult(
[SearchResultItem('aseem wangoo', 'xyz', 'abc')]);
});
expect(searchBloc.state, emitsInOrder([noTerm,loading,populated]));
searchBloc.onTextChanged.add(term);
});
- We add a string into our onTextChanged sink
- Then we fake the response using Mockito stubbing
- thenAnswer: Store a function which is called and the return value will be returned. (in our case SearchResult)
- Expected states are noTerm, loading, and populated.
5. Check for no results from API
test('emit empty state if no results', () async {
final term = 'aseem';
when(searchBloc.api.search(term)).thenAnswer(
(_) async => SearchResult([]),
);
expect(searchBloc.state, emitsInOrder([noTerm, loading, empty]));
searchBloc.onTextChanged.add(term);
});
- We add a string into our onTextChanged sink
- Then we fake the response using Mockito stubbing (an empty SearchResult)
- Expected states are noTerm, loading, and empty.
6. Check for API down
test('emit error state if API is down', () async {
final term = 'aseem';
when(searchBloc.api.search(term)).thenThrow(Exception());
expect(searchBloc.state, emitsInOrder([noTerm, loading, error]));
searchBloc.onTextChanged.add(term);
});
- We add a string into our onTextChanged sink
- Then we throw an exception using Mockito stubbing
- Expected states are noTerm, loading, and error.
7. Check if the stream is closed
test('stream is closed', () async {
when(searchBloc.dispose());
expect(searchBloc.state, emitsInOrder([noTerm, emitsDone]));
});
- We dispose of the bloc
- Expected states are noTerm and emitsDone.
- emitsDone: Returns a StreamMatcher that asserts that the stream emits a “done” event.
Hosted URL : https://web.flatteredwithflutter.com/#/
Posted on February 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.