FilledStacks

Share this post

The 3 blockers Flutter developers face when writing unit tests

filledstacks.substack.com

Discover more from FilledStacks

Weekly newsletter focused on using software engineering fundamentals to build high quality front-end applications.
Over 1,000 subscribers
Continue reading
Sign in

The 3 blockers Flutter developers face when writing unit tests

Let's address them, look at examples, and define some steps to take

Dane Mackier
Sep 1, 2023
9
Share this post

The 3 blockers Flutter developers face when writing unit tests

filledstacks.substack.com
Share

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:

  1. 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');
});
  1. 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);
});
  1. 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:

  1. If the request is incorrect, whether it be the url, or the headers, this function is to blame

  2. If we responded the wrong way based on the response code, this function is to blame

  3. If deserialization of the response body fails, this function is to blame

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

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

  2. Remove the statusCode check, that can be done in the httpService created above and can throw exceptions if it’s an error code. Then we can handle the exception in the calling function.

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

Thanks for reading FilledStacks! Subscribe for free to receive new posts and support my work.

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.

Thanks for reading FilledStacks! Subscribe for free to receive new posts and support my work.

Thanks for reading FilledStacks! Subscribe for free to receive new posts and support my work.

Good luck,

Chat again next week.


Subscribe below to get these articles directly in your inbox every week.

9
Share this post

The 3 blockers Flutter developers face when writing unit tests

filledstacks.substack.com
Share
Comments
Top
New
Community

No posts

Ready for more?

© 2023 FilledStacks (Pty) Ltd
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing