Engineering

Flutter Unit Testing

Testing is often an overlooked part of app development. Unit tests are the most trivial testing software you can implement in your project anytime. You only need the flutter_test package in your dev dependencies (it's added to every project by default).

Unit tests help to maintain your code, discover bugs quicker and ensure the quality of your code. They're also a key factor when working with legacy code.

How to write unit tests

Let's say you have a simple method that calculates 6 * 7:

The test for it would look somehow like this:

Pretty simple, but in the real world, you'd have a class that calls some other method in its dependency and returns some data. More or less something like this:

First, we need to create an instance of our class, but we have one problem since it relies on NetworkManager. We could also create a NetworkManager class, but that's not a point of unit tests. When unit testing, you should always limit what you're testing to the very minimum, so you know what failed precisely.

With the help come mocks, which allow you to create simulated objects that imitate the behavior of real objects in controlled ways. You'll need to add mocktail to your packages. Then, create a mock class that you want to imitate, like the one below:

Now you can use it in your tests:

Let me explain what happens here. First, you create a mocked version of NetworkManager, then pass it to the class you want to test. Subsequently, you tell the NetworkManager what he needs to respond with when calling the getData method. Finally, you execute the function and compare the data.

There's also one cool thing that you can do with mocks. You can verify if the methods inside of them were called or not. It comes in handy once your method doesn't return anything (void method).

Here we're verifying that the getData method was called once and that there were no more calls on our mockNetworkManager.

Working with unit tests

Unit tests are a great way to keep your project together. Let's say you change one of the methods to do something else. For example, the calculate method now calculates different equations 3 * 7 - the tests won't pass now, and you will need to update them.

Why is this useful? This example asks you to double-check your changes, but let's see what happens if something changes in the getParsedData method. There are way more scenarios that can go wrong here. For example, you could call getData twice - the tests will fail.

You could also call another method from NetworkManager instead of using getData and the tests will also fail. You're notified about the changes and assured the changes are made for a reason.

Useful methods

  • emitsInOrder - helpful in testing streams or blocs
  • expectLater - works like expect, but completes when a matcher has finished matching
  • pumpEventQueue - waits for an event loop to run 20 times (default), helpful when waiting for some async work to complete