Testing BLoC in Flutter

aseemwangoo

aseem wangoo

Posted on February 27, 2021

Testing BLoC in Flutter

In case it helped :)
Pass Me A Coffee!!

Website: https://flatteredwithflutter.com/testing-bloc-in-flutte/

We will cover briefly about

  1. Convention for Tests
  2. Mocking Github API
  3. 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.

Testing BLoC in Flutter

  • 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 MockGithubSearchImplwhich extends Mock 
  • 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

  1. 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/#/

Source code for Flutter Web App..

💖 💪 🙅 🚩
aseemwangoo
aseem wangoo

Posted on February 27, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related