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
![](https://res.cloudinary.com/practicaldev/image/fetch/s--NL4jMVQr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/1600/1%2A5I2qKaWZ8F7We_yv3h6m0w.png)
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/#/
Top comments (0)