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
fpdart
easy and predictable - Learn how to use the
mocktail
package to create mocks - Test the
getAllEvent
function usingmocktail
There is more ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
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 ๐
fpdart is completely tested internally to make sure that every type and method works in all situations (screenshot from fpdart v1.1.0)
There is more ๐คฉ
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Implement tests using mocktail
We want to test the getAllEvent
function:
- Call the correct
getAll
method fromStorageService
- Return the correct
QueryGetAllEventError
whenStorageService
throws - Return
SuccessGetAllEventState
when the request is successful - Make sure
SuccessGetAllEventState
contains 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
storageService
instance when runninggetAllEvent
- Use
verify
andcalled
to test thatgetAll
is 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-case
syntax introduced in Dart 3 to extracteventEntityList
inside the if statement.Check out Records and Pattern Matching in dart - Complete Guide for more details.
With this all the tests are completed!
All tests are passing, we are confident that the app works correctly in all situations
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.