Flutter Unit Test Guidelines

Amit Singh
4 min readSep 22, 2023

--

During your programming journey, you will build hell lots of applications irrespective of any technology that you use, and getting these things done is one thing but doing it the right way is what matters.

Hello guys, this is Amit Singh and in this post, we are going to discuss some of the testability Guidelines for Unit tests.

So let's start …

Any code isn’t testable. This covers the main points to ensure your codebase is ready to accommodate and benefit from unit tests.

What makes a code testable?

Code is considered testable when it is designed in a way that allows automated tests to be easily created and run. This means that the code should be:

  1. Modular, with clear separation of concerns and dependencies.
  2. Passing dependencies from constructors to allow mocking.
  3. Following the Single Responsibility Principle (SRP).

When code is testable, it allows developers to catch bugs and issues early on in the development process and ensure that changes to the code don’t introduce new problems. This can save time and resources in the long run, as it helps to prevent issues from appearing in production and reduces the need for manual testing.

Practices to follow

1. Use Dependency Injection

Dependency injection is a design pattern that allows us to inject dependencies into our code. This makes it easier to write unit tests because we can replace dependencies with mocks or stubs. In Flutter, we can use the provider package to implement dependency injection. Here is an example:

class UserService {
final HttpClient httpClient;

UserService(this.httpClient);

Future<User> getUser(String id) async {
final response = await httpClient.get('/users/$id');
return User.fromJson(response.data);
}
}

In this example, we are injecting a HttpClient dependency into the UserService. This makes it easier to test because we can replace HttpClient it with a mock during testing.

2. Use Separation of Concerns

Write small and focused functions: Functions that do one thing and do it well are easier to test. Avoid large functions that perform multiple tasks.

bool isEmailValid(String email) {
final regex = RegExp(r'^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$');
return regex.hasMatch(email);
}

3. Avoid global and static states

Global state makes it difficult to test your code in isolation because changes in one part of the code can affect other parts. In addition to that, they can’t be mocked directly.

class Counter {
static int count = 0;

static void increment() {
count++;
}
}​

4. Don’t call another function within the same class.

Writing unit tests for functions calling other functions within the same class can become messy and difficult to manage, especially because you can’t mock or simulate their output.

class Example {
int add(int a, int b) {
return a + b;
}

int multiply(int a, int b) {
int result = 0;
for (int i = 0; i < a; i++) {
result = add(result, b);
}
return result;
}
}

Suppose multiply is the main function, while add is the sub-function being called inside multiply. It becomes hard to write a test for the multiply function on its own, because it depends on the add function's implementation.

Using Dependency Injection:

class Adder{
int add(int a, int b) {
return a + b;
}
}

class BetterExample {
final Adder _adder;

BetterExample(this._adder);

int multiply(int a, int b) {
int result = 0;
for (int i = 0; i < a; i++) {
result = _adder.add(result, b);
}
return result;
}
}

Now multiply depends on an instance of the Adder class, which can be easily mocked for testing. The test for the multiply function can be written without having to worry about testing the add function.

5. Use SOLID Principles

SOLID principles are a set of software design principles that help us write code that is maintainable, scalable, and testable.

  1. Single Responsibility Principle (SRP)
    A class/method should have only one reason to change.
  2. Open/Closed Principle (OCP)
    A class should be open for extension but closed for modification.
  3. Liskov Substitution Principle (LSP)
    Subtypes must be substitutable for their base types.
  4. Interface Segregation Principle (ISP)
    Clients should not be forced to depend on methods they do not use.
  5. Dependency Inversion Principle (DIP)
    High-level modules should not depend on low-level modules. Both should depend on abstractions.

For more details, please read: Solid Principles Blog by Amit Singh

If you made till here I think you have done a great job learning all these best practices.

If any of my posts help you in any way, you can consider buying me a cup of coffee. I will personally thank you 🙏.

https://www.buymeacoffee.com/amit08cse53

Thanks, guys, That’s all for now, Make sure to give a clap 👏 and leave some engagement. If there is any correction or feedback feel free to leave a comment as well.

Check out similar posts from CodingWithAmitSingh. Connect me on Twitter or Linkedin

--

--