Testing your code is generally a good idea, but choosing which test to use in Flutter can get confusing. Specifically, when should you use widget tests vs unit tests since they often overlap?
Here are some questions we will answer
- What is a unit test?
- What is a widget test?
- When to use which?
- Don’t widget and unit tests sometimes test the same logic?
We have a simple framework for how you should think about these.
The Framework:
- Unit Tests -
input <-> output
- Widget Tests -
action <-> result
Before we explain this Framework and how to apply it, let’s ensure we understand what a Unit and Widget test is.
Unit Test
A unit test is for testing a small “unit” of code in isolation. Typically, a “unit” is a function or method.
Here is the simplest example I can give. This method takes two inputs, adds them together, and returns the output.
Here are some example tests you would use to verify that this function acts the way you expect. Notice that we handle edge cases like negative and big numbers.
Widget tests
Widget tests are more cumbersome than unit tests. While unit tests test a single function (or “unit”), widget tests verify the widget functions correctly.
Let’s take this counter widget as an example. We use the MVVM architecture here, which we teach in the Best Flutter Course.
The ViewModel contains the business logic for the counter, and this widget interacts with the ViewModel to increment and display the value. It is very similar to the Flutter starter application but with MVVM.
A widget test for this widget might look something like this. We first ensure that the counter starts at 0, and then when the button to increment is tapped, we ensure the value increases accordingly.
So what is the Framework?
You might have noticed that there was no unit test written in the counter example. The incrementCounter
method has not been tested. But it also kinda is?
Unit Test Framework
Let’s finally get to the actual Framework and how to think about which test to use for what situation.
For unit tests, think of input <-> output
. Now, what do we mean by this? Let’s return to the first example with the sum
function. This function is self-contained and only handles inputs and outputs.
That means that as long as I don’t change this function, I can change any other code in my application, and this function and its test will never fail. The only time the test will fail is if I change the logic for how to do addition.
In short, you shouldn’t break all your tests when you refactor.
Widget test framework
When we tested the increment action in the widget test, this was not input <-> output
. It relies on other files in the application (like the ViewModel), so it’s not self-contained. It also doesn’t really have inputs. Instead, actions are tested to ensure they give the expected results.
So that’s the widget test framework: action <-> result
.
The widget test does the action of tapping on the button, which results in the number going up.
Although we are testing the CounterPage
widget, it also tests the ViewModel business logic for incrementing and the correct use of the ValueNotifier
.
There is no reason to write a unit test for the counter logic here since the widget test already covers it.
If I happen to refactor the ViewModel to use a ChangeNotifier
, nothing here would break since we are not directly unit testing the incrementCounter
method and asserting on the counter ValueNotifier
.
The widget test in this scenario had a higher value than a single unit test.
With a single widget test in the previous examples, we have 100% coverage for the ViewModel.
Without a single unit test.
Why use the Framework?
Some business logic is critical. You want to make sure inputs give the correct output every time. However, for most apps, testing that the user’s action leads to the correct result makes sense.
The Framework:
- Unit Tests -
input <-> output
- Widget Tests -
action <-> result
Upsides
- You can more freely refactor
- Clear approach for when to use what
- Less overlap between unit and widget tests
Downside
Widget tests are slower than unit tests, but this is very hard to justify, given the upsides.
Breaking the rules
I want to make it clear, though, that there are certain scenarios where you can break out of the Framework. As with anything in coding, nothing is set in stone. But this approach makes it very clear for us when to use unit vs widget tests.
So, in short.
Unit tests, test for input <-> output
Widget tests, test for action <-> result
So don’t sleep on Widget tests
YouTube Video
Get articles right in your inbox
No spam, unsubscribe anytime. We treat your inbox with respect.