We will cover briefly:
- Setup Mockito
- Mocking repository using Mockito
- Write unit tests for repository
- Write unit tests for the view model
- (Optional) Code coverage
Setup Mockito
There are often times when unit tests depend on classes that fetch data from live web services or databases. This is inconvenient for a few reasons:
- Calling live services or databases slows down test execution.
- It is difficult to test all possible success and failure scenarios by using a live web service or database.
You can mock dependencies by creating an alternative implementation of a class. by making use of the Mockito package as a shortcut.
Following is the general idea behind Mockito
Rather than relying on a live web service or database, you can “mock” these dependencies. Mocks allow emulating a live web service or database and return specific results depending on the situation.
We use the mockito
package for writing unit tests and for generating the implementation it relies on build_runner
. Add the plugins to your pubspec.yaml
file
# pubspec.yaml
dependencies:
mockito: ^5.1.0
dev_dependencies:
build_runner: ^2.1.7
Detailed description here.
Mocking repository using Mockito
Before starting this, let's see the architecture which we used for our app
For each of the screens, we have the following:
- Repository: Which handles our business logic
- ViewModel: Bridge between the UI and the repository
- View: Actual screen visible to the user
- Model: Response being cast to models, in order to be consumed in the view.
Repository tests using Mockito
- This is what our home repository looks like,
abstract class HomeRepo {
Future<CarouselModel> fetchData();
}
class HomeRepoImpl extends HomeRepo {
@override
Future<CarouselModel> fetchData() async {
await Future.delayed(const Duration(milliseconds: 1800));
final resp = await rootBundle.loadString('assets/data/first_screen.json');
return carouselModelFromJson(resp);
}
}
In the above snippet, we are creating a manual delay in order to simulate the network delay, and afterward, we simply fetch our JSON file (present under the assets).
- For mocking our repository, we first create a file
home_repo_test.dart
under test folder as below
Note: The tests should follow
test/folder_name/<name>_test.dart
pattern
- Inside the file, we add the annotation
@GenerateMocks([HomeRepoTest])
to the main function to generate aMockHomeRepoTest
class withmockito
.
By annotating a library element (such as a test file’s
main
function, or a class) with@GenerateMocks
, you are directing Mockito's code generation to write a mock class for each "real" class listed, in a new library.
// IMPORTS OMITTED FOR BREVITY
class HomeRepoTest extends Mock implements HomeRepo {}
@GenerateMocks([HomeRepoTest])
Future<void> main() async {
}
HomeRepoTest: The mock class which will be referred to in the generated class
MockHomeRepoTest: The generated mock class which implements the functions inside the HomeRepo
Next, generate the mocks running the following command:
flutter pub run build_runner build
The generated mocks will be located in home_repo_test.mocks.dart
. Import this file to use them inside your test file.
- We should see something like this if all the above steps were done correctly
Write unit tests for repository
Let’s write the unit tests for the repository now, with all the necessary setup done in the previous step.
- We will initialize our mock repository inside the
setUpAll
Future<void> main() async {
late MockHomeRepoTest homeRepo;
setUpAll(() {
homeRepo = MockHomeRepoTest();
});
test('test fetchData', () async {
final model = CarouselModel();
when(homeRepo.fetchData()).thenAnswer((_) async {
return model;
});
final res = await homeRepo.fetchData();
expect(res, isA<CarouselModel>());
expect(res, model);
});
test('test fetchData throws Exception', () {
when(homeRepo.fetchData()).thenAnswer((_) async {
throw Exception();
});
final res = homeRepo.fetchData();
expect(res, throwsException);
});
}
- We have two tests inside the repository: For the first one, we use the
when
,thenAnswer
APIs provide a stubbing mechanism. Once stubbed, the method will always return the stubbed value regardless of how many times it is called.
when: Creates a stub method response. Mockito will store the fake call and pair the exact arguments given with the response. The response generators from Mockito include
thenReturn
,thenAnswer
, andthenThrow
.
thenAnswer: Stores a function which is called when this method stub is called. The function will be called, and the return value will be returned.
We return the CarouselModel
from the stub and use the expect to assert that actual
matches matcher
.
In our case, we expect it to be of type CarouselModel
using isA
- For the second test, we throw an Exception from the stub. We expect the
actual
matchesthrowsException
which is a matcher for functions that throw Exception.
Write unit tests for the view model
We will first initialize our mock home repository and the view model inside the setUpAll
We pass in the Mock repository MockHomeRepoTest
to our view model, as it expects the repository as a required parameter.
Future<void> main() async {
late MockHomeRepoTest homeRepo;
late HomeViewModel viewModel;
setUpAll(() {
homeRepo = MockHomeRepoTest();
viewModel = HomeViewModel(repo: homeRepo);
});
test('test fetchData', () async {
final model = CarouselModel();
when(homeRepo.fetchData()).thenAnswer((_) async {
return model;
});
await viewModel.fetchData();
expect(viewModel.homeModel, model);
});
}
- In the test above, we use the
when
,thenAnswer
APIs
We call the homeRepo.fetchData()
using the when
and return the stub response as the CarouselModel
Next, we call the actual function fetchData()
from inside the view model using viewModel.fetchData()
Finally, we expect it to be of type CarouselModel
in the matcher, since we are setting the response from the repository to the model.
// HomeViewModel
Future<void> fetchData() async {
isLoading = true;
_homeModel = await repo.fetchData();
isLoading = false;
notifyListeners();
}
Code coverage
In case you are using Android Studio, you can simply right-click on the test and run tests with Coverage
For VSCode, install the following extensions
Execute the following command
flutter test --coverage
This will generate the lcov.info
which contains the coverage information.
Now, click on the testing icon on the left side of the VSCode and you can see your code coverage. Good article here
Top comments (0)