Testability Guidelines
Any code isn’t testable. This document 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:
Modular, with clear separation of concerns and dependencies.
Passing dependencies from constructors to allow mocking.
Following 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:
In this example, we are injecting an HttpClient
dependency into the UserService
. This makes it easier to test because we can replace the HttpClient
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.
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.
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.
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:
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 to write code that is maintainable, scalable and testable. A summary:
Open/Closed Principle (OCP)
A class should be open for extension but closed for modification.
Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types.
Interface Segregation Principle (ISP)
Clients should not be forced to depend on methods they do not use.
Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions.
For more details, please read Unit Testing Introduction.