Testing fpdart code using mocktail | Fpdart and Riverpod Functional Programming in Flutter

Sandro Maglione

Sandro Maglione

Mobile development

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.

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 using mocktail

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

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

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)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.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.

Implement tests using mocktail

We want to test the getAllEvent function:

  • Call the correct getAll method from StorageService
  • Return the correct QueryGetAllEventError when StorageService throws
  • Return SuccessGetAllEventState when the request is successful
  • Make sure SuccessGetAllEventState contains the correct data returned by StorageService

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:

get_all_event_test.dart
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:

storage_service.dart
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 running getAllEvent
  • Use verify and called to test that getAll 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:

get_all_event_test.dart
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:

get_all_event_test.dart
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:

get_all_event_test.dart
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 extract eventEntityList 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 situationsAll 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.

๐Ÿ‘‹ใƒปInterested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 700+ readers.