This is the sixth part of a new series in which we are going to learn how to build a safe, maintainable, and testable app in Flutter using fpdart and riverpod.
We will focus less on the implementation details, and more on good practices and abstractions that will helps us to build a flexible yet resilient app in Flutter.
- Read part 1: Project Objectives And Configuration
- Read part 2: Data model and Storage interface
- Read part 3: Dependencies and Errors in Functional Programming
- Read part 4: How to use fpdart and riverpod in Flutter
- Read part 5: Business logic with fpdart and the Do notation
As always, you can find the final Open Source project on Github:
In this article we are going to:
- Define the principles that make testing with
fpdarteasy and predictable - Learn how to use the
mocktailpackage to create mocks - Test the
getAllEventfunction usingmocktail
There is more ๐คฉ
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
Recap: Complete app using fpdart
In the last post we officially completed the implementation of the app business logic using fpdart.
We implemented the getAllEvent function. We learned about the ReaderTask and TaskEither types and how they can be used to define dependencies and handle errors.
Finally, we used the new Do notation with the .Do constructor to execute the logic and return GetAllEventState.
With all of that the app is complete โ๏ธ
Wait! How do we make sure that everything works correctly?
The very last step is testing. We are going to focus on it today ๐
Testing with fpdart: It's easy
There is not much to say really ๐๐ผโโ๏ธ
Testing with fpdart and functional programming becomes an easy and satisfying process:
- Pure functions: given the same input, the function returns always the same output. This makes testing predictable. Furthermore, it is not necessary to setup any environment or external variable, since the result of every function solely depends on the inputs
- Immutability: every function returns a new copy of the original data, without mutations. We only need to test that the output value is correct, without worrying about inputs or any other part of the system
- Dependency injection: in functional code every dependency is explicit. This makes it possible to provide mocks for every dependency used by a function, which makes testing predictable in all its aspects
Testing is painful ๐ฎโ๐จ Do you know why? Implicit dependencies ๐ For example, how do you test the code below ๐๐งต (TLDR: You can't ๐ซ)
If that wasn't enough, by using fpdart's types you are guaranteed that all the above principles always apply.
Furthermore, all fpdart types are completely tested inside the library, with more than 1000 tests verified ๐
There is more ๐คฉ
Every week I dive headfirst into a topic, uncovering every hidden nook and shadow, to deliver you the most interesting insights
Not convinced? Well, let me tell you more about it
Implement tests using mocktail
We want to test the getAllEvent function:
- Call the correct
getAllmethod fromStorageService - Return the correct
QueryGetAllEventErrorwhenStorageServicethrows - Return
SuccessGetAllEventStatewhen the request is successful - Make sure
SuccessGetAllEventStatecontains the correct data returned byStorageService
We are going to use the mocktail package for mocking StorageService.
Combining fpdart and mocktail makes testing easy and fun ๐
Mocking StorageService
Using mocktail we can stub and verify calls.
We start by creating a Mock implementation for StorageService:
class StorageServiceMock extends Mock implements StorageService {}By providing an instance of StorageServiceMock to the getAllEvent function we can verify the correct method calls inside StorageService, as well as provide custom returns values.
Let's now implement the first test: call the correct method from StorageService.
We expect getAllEvent to call exactly 1 time getAll from StorageService:
abstract class StorageService {
Future<List<EventEntity>> get getAll;
Future<EventEntity> put(String title);
}We use verify from mocktail to achieve this:
test('should call "getAll" from StorageService one time', () async {
final storageService = StorageServiceMock();
await getAllEvent.run(storageService);
verify(() => storageService.getAll).called(1);
});- Create an instance of
StorageServiceMock(storageService) - Provide the
storageServiceinstance when runninggetAllEvent - Use
verifyandcalledto test thatgetAllis called exactly 1 time
Verify return types using when and expect
The other 3 tests are also easy to implement.
We use mocktail to provide a custom return value for getAll from StorageService. We then verify that getAllEvent behaves as expected and returns the correct value.
When getAllEvent returns QueryGetAllEventError it means that calling getAll throws a error.
We therefore use when from mocktail to test this situation:
test('should return QueryGetAllEventError when getAll throws', () async {
final storageService = StorageServiceMock();
when(() => storageService.getAll).thenThrow(Exception());
final result = await getAllEvent.run(storageService);
expect(result, isA<QueryGetAllEventError>());
});By using thenThrow we specify that calling getAll should throw an error. We then execute getAllEvent and verify that result is indeed an instance of QueryGetAllEventError.
We do the same to verify a successful response. Instead of thenThrow we use thenAnswer:
test('should return SuccessGetAllEventState when the request is successful', () async {
final storageService = StorageServiceMock();
when(() => storageService.getAll).thenAnswer((_) async => []);
final result = await getAllEvent.run(storageService);
expect(result, isA<SuccessGetAllEventState>());
});This test verifies that we get an instance of SuccessGetAllEventState when the request is successful.
The last test verifies instead that the value inside SuccessGetAllEventState is the same list returned by getAll from StorageService:
test('should return the list returned by getAll when the request is successful', () async {
final storageService = StorageServiceMock();
final eventEntity = EventEntity(0, 'title', DateTime(2020));
when(() => storageService.getAll).thenAnswer((_) async => [eventEntity]);
final result = await getAllEvent.run(storageService);
if (result case SuccessGetAllEventState(eventEntity: final eventEntityList)) {
expect(eventEntityList, [eventEntity]);
} else {
fail("Not an instance of SuccessGetAllEventState");
}
});This test uses the new
if-casesyntax introduced in Dart 3 to extracteventEntityListinside the if statement.Check out Records and Pattern Matching in dart - Complete Guide for more details.
With this all the tests are completed!
This is it for part 6!
Now our app is complete in all its aspects! By using fpdart and mocktail we were able to implement and test the business logic of the app for all possible cases and return types.
The principles of functional programming make testing a breeze. We have control over all the dependencies and we can provide stubs and mocks for every method.
We are now confident that the app behaves as expected ๐
You can subscribe to the newsletter here below for more tutorials and articles on functional programming and fpdart ๐
Thanks for reading.
