The 3 blockers Flutter developers face when writing unit tests
Let's address them, look at examples, and define some steps to take
I’ve had the opportunity to work with many developers, in my own team, as well as my client’s teams.
Whenever unit testing came up I always heard the same 3 questions and concerns. In this article, I would like to address each of those with the solution I used to solve them, examples of how to do it, and action items you can take today to get started.
The goal is not to unit testing mastery, it’s to practice writing testable code.
If your code is unit-testable, you can write integration tests, which is the most valuable form of testing.
The attempt to unit test your code will force you to write code that is less coupled with well-separated layers of concern. Not writing testable code means you’ll forever be stuck with the problem of losing developer hours to manual testing. This is very unfortunate because this problem has been solved multiple decades ago and is easier to get started with than many developers think.
Let's look at the most common questions and concerns about writing tests, hopefully, this helps you start your journey.
I don’t know what to test:
You can’t do something if you don’t know where to start, so this is usually the first question. The goal is to create the habit of writing testable code.
Here’s my list of types of tests to write, in the order you should attempt to write them, with some examples:
X in equals Y out: This is the most common form of unit tests and is the easiest to write. You call a function with an argument and you check the result. Like below:
test('When called with "Subscribe to FilledStacks", should return "StFS"', () {
final utils = StringUtils();
final result = utils.convertStringToId('Subscribe to FilledStacks');
expect(result, 'StFS');
});
Calling function X, changes the state to Y: Even having the ability to write this test means you’ve separated all your state logic from your UI code, and that’s a major step for your Flutter code. Here’s an example:
test('When calling selectItem with index 2, isItemSelected with 2 should return true', () {
final viewModel = MyListViewModel();
viewModel.selectItem(index: 2);
final result = viewModel.isItemSelected(index:2);
expect(result, true);
});
Confirm conditional logic works: With this type of test you take a function that has many execution paths and write a test to confirm that each of them are executed given the right state or input. The most important case to cover is the default path, I’ve seen this cause the most bugs.
Example for this one not required, it looks the same as X in Y out, but multiple tests with different X’s in and different Y’s out
Action Item for you
Identify any functions in your code that takes in a value and returns a different value
Identify any code that calls a function and expects state to be updated
Identify any function that has multiple if statements of switch expressions
Write 1 test for each of those functions (If you’re struggling with it, reply to this email with the function and I’ll help you test it)
I don’t know how to test this function:
This happens when a function has too many responsibilities. To identify a responsibility you identify which part of the code is to blame if something had to break. Let’s take this function as an example:
class HomeViewModel extends BaseViewModel {
List<Artist> _artists = [];
Future<void> fetchArtists() async {
// #1: Structure the http request
final response = await http.get(Uri.https('venu.is', '/artists'));
// #2: Validate the request is successful
if (response.statusCode < 400) {
// #3: Convert the response into a map
final responseBodyAsMap = jsonDecode(response.body);
// #4: Get relevant data from the response
final artistMaps =
responseBodyAsMap['data'] as List<Map<String, dynamic>>;
// #5: Deserialize into a list of artists
_artists = artistMaps.map(Artist.fromJson).toList();
}
}
}
In this function I’ve added a comment for each of the responsibilities. Now, it doesn’t mean they don’t belong together, but it does make it more difficult to test. Let’s go through each responsibility:
If the request is incorrect, whether it be the url, or the headers, this function is to blame
If we responded the wrong way based on the response code, this function is to blame
If deserialization of the response body fails, this function is to blame
Along with 5, if we fail to construct the model, this function is to blame
These mixed responsibilities makes this function very difficult to unit test. There are numerous changes to make here but I’ll go over the most important ones.
Put the
http.get
function call into a service class and use that. This would allow you to mock the response so your unit test always uses the same response data for the test, making it a deterministic test. Deterministic means, given the same input you can consistently predict the result.Remove the
statusCode
check, that can be done in thehttpService
created above and can throw exceptions if it’s an error code. Then we can handle the exception in the calling function.Serialization of our http requests is usually done in the
httpService
as well, but to split the responsibility in this case, we can create a utility class that takes in a String and returns a typed model. Then we can test it using the X in, Y out test types as shown in part 1.
The new function will look something like this.
class HomeViewModel extends BaseViewModel {
final _httpService = locator<HttpService>();
List<Artist> _artists = [];
Future<void> fetchArtists() async {
try {
final response = await _httpService.getArtists();
_artists = ConversionUtils.convertBodyToTypedArtist(response.body);
} catch(e) {
// Handle the error here based on the exception that checks response code
}
}
}
This isn’t how I would have actually written the function, but it serves the purpose of the example. This now shifts the responsibilities out of this function and allows you to simply test state, with fixed, and predictable outcomes. When calling fetchArtists() with a responseX, artists should be equal to resultY
. There’s nothing else to test in there, because it has no other responsibility besides setting the state of the artist variable when it gets a result.
All the other classes will now how their own set of unit tests to confirm that they’re handling their responsibilities.
Action Items
Take the function you worked on last
Identify how many responsibilities it has
Move only one of those responsibilities to a different class or function
Write a single test for that class or function, only testing that 1 responsibilitiy
Repeat as many times as you’d like
If you’re struggling to take the step, reply to this mail with the function you’re struggling with, some context, and I’ll help you write a test for it.
When do I write a test?
After you know your code works.
When you start with unit testing you won’t be good at it. This means you’ll make mistakes, you’ll write tests that are not good, and you’ll even write tests that don’t test what you want to test. This means it’ll slow you down. I usually recommend that beginners to testing write tests after they know their code works.
Use it as a way to “prove” that your code does what the requirements ask from it.
Eventually, you’ll want to write the tests as you write your code, and if you enjoy it, do what I do and write the tests before you write the code (in certain cases).
Action Items
After completing a piece of code and you know it works
Go through the action items in the 2 concerns before this one and apply them
Writing testable code is how you unlock speed, I hope you take the time to take on these first steps. You’ll deliver more, faster, and at a higher quality.
Good luck,
Let’s work together
Book a 1-on-1 call with me to take the next steps for your Flutter project.