Write better Flutter unit tests without mockito

Published on

Tests are one of most controversial topics in the flutter community and in the software development community in general. This whole article is based on our own experience and our own way of doing things. It's what we found to be the best way to test our apps and to make sure that our tests are not slowing us down.

Why do we write tests ?

The first question we need to ask ourselves is why do we write tests ?
The answer is simple, we write tests to make sure that our app is working as expected. But also to make sure that our app will keep working as expected when we will add new features or when we will refactor our code.

Don't try to cover everything with tests. It's impossible and it's not the goal of tests. Code coverage is not a good metric to measure the quality of your tests. It's just a metrics that tells you how much of your code is covered by tests, but it doesn't tell you if your tests are good or not.

Good tests helps you to find bugs and regressions. It also helps you to write better code and to have a better architecture.

On Flutter this also reduce the requirement of manual testing.
As your app grows you will have more and more features and it will be harder to test everything manually. It's also sometimes hard to launch an app in a specific state to test a specific feature. With tests you can test everything automatically and you can test everything in a specific state within seconds.

I personnaly think that you sometimes lose more time testing manually than taking 2 minute to write a test.

What is a unit test

First of all. A unit test is NOT a test for a single function.

How do we write efficient flutter tests

1. Don't use mockito or any other mocking library

Mocking libraries were made with a good intention. Trying to make our tests easier to write. But in the end, they are making our tests harder to read and harder to maintain.

When you are using a mocking library, you are coupling your tests to the implementation details of your code. If you change the implementation of your code, you will have to change all the tests that are using this code. And that's not what we want.

mockito

As your application grows, you will have more and more "when" mocking calls.

Also mocking libraries makes tests harder to read and harder to understand. When you are reading a test, you want to understand what is the input and what is the output. Not how an API works.

2. Test scenarios, not functions

As I said earlier, we want to test scenarios, not functions.
We have an input and we want to make sure that we have the expected output.

Let's take an example. We are building a calculator app and we want to test the addition function.

As we want to test scenarios I like to write it clearly in the test name.
Like this:

test('calculator is empty, add 1 then add 2 => result equals 3', () {
  final calculator = Calculator();
  calculator.add(1);
  calculator.add(2);
  expect(calculator.result(), 3);
});

3. Use a fake implementation instead

Instead of using a mocking library, we will use a fake implementation of our dependencies. This way we will be able to test our code without being coupled to the implementation details of our code.

Let's take an example. We are building a calculator app and all calculation are made through an API. As I said earlier we don't want to make network calls in our tests. So we will create a fake implementation of our API. "Test only what you own" is a good rule to follow.

class FakeCalculatorApi implements CalculatorApi {
  
  Future<int> add(int a, int b) async {
    return a + b;
  }
}

Now we can use this fake implementation in our tests.

test('calculator is empty, add 1 then add 2 => result equals 3', () {
  final calculator = Calculator(api: FakeCalculatorApi());
  calculator.add(1);
  calculator.add(2);
  expect(calculator.result(), 3);
});

Here is a more complex test without mocks for a signup. You won't have any "when" calls. You will just have the input and the output. And that's all we want.

mockito

You can store your fake implementations in the test folder in your project. Fake can be used in multiple tests, so it's a good idea to store them in a single place.

So now instead of having to mock our API everywhere, we just have to use our fake implementation. As flutter makes it really easy to test like if we were actually running the app, I like to create tests that are as close as possible to the user behavior.

Fakes helps maintainability

As our fakes implements the same interface as our real implementation. You will directly be able to see if your fake is still up to date with your real implementation. When you add a method to your real implementation, you will have to add it to your fake implementation and all your tests should still pass.

As your tests are not coupled to the implementation details of your code, you will be able to refactor your code without having to change your tests. You can use this new function in your code. As long as it won't change the result, your tests will still pass.

What if I need to test fail cases ?

You can write another implementation of your fake API that will throw an exception. This way you will be able to test fail cases.

Fakes can have multiple implementations. But also more complex implementations. Some fakes tries to simulate the behavior of the real implementation. For example my fake authentication API will return a fake token if the user previously called login with the right credentials.

5. Write a test for each issues

We don't speak about cosmetic issues here. We speak about bugs and regressions. If your app is in production and you have a bug. The first thing you should do is to write a test for this bug. This way you will be able to reproduce the bug and to make sure that it won't happen again.

6. Write your tests before writing your code (TDD)

This is for me one of the most important point.

This one may sounds weird the first time you hear it. But it's actually a good practice. When you are writing your tests before writing your code, you are forced to think about the API of your code. You are forced to think about how you want to use your code. This way you will have a better code and architecture.

You don't have to do this all the time.
Start to do it on small features and you will see that it's actually a good practice. There is a lot of articles about TDD, so I won't go into details here.

7. Don't test the UI

I don't like to test design details. But Flutter has an amazing test framework that allows you to test your UI. If you test your UI, be sure to test details that really matters. UI is made for change to adapt to the user needs.

You should be able to change UI often and easily.

Conclusion

I hope that this article will help you to write better tests or stop fearing tests. Tests can be amazing once you start getting some experience with them. It saved me a lot of time and I believe I've learned a lot by writing them.

If you have any questions or if you want to share your experience with tests, feel free to reach me on Twitter.

Create a 5 stars app using our Flutter templates

Check our flutter boilerplate
kickstarter for flutter apps
Read more
You may also be interested in
Our Flutter Template is Here!  blog card image
Our Flutter Template is Here!
Published on 2023-10-10
Flutter theme made easy  blog card image
Flutter theme made easy
Published on 2023-10-23
FlutterKit by Apparence.io © 2023. All rights reserved