When to use Widget vs Unit Tests in Flutter

You should probably be writing more widget tests.

A flutter logo with the words testing framework

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.

int sum(number, number2) {
return number + number2;
}

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.

test('should return 5 when summing 2 and 3', () {
expect(sum(2, 3), equals(5));
});
test('should return 0 when summing 0 and 0', () {
expect(sum(0, 0), equals(0));
});
test('should return -5 when summing -2 and -3', () {
expect(sum(-2, -3), equals(-5));
});
test('should handle large numbers correctly', () {
expect(sum(1000000, 2000000), equals(3000000));
});

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.

class _CounterPageState extends State<CounterPage> {
final CounterViewModel viewModel = CounterViewModel();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ValueListenableBuilder<int>(
valueListenable: viewModel.counter,
builder: (context, value, _) {
return Text('Counter: $value');
},
),
ElevatedButton(
onPressed: viewModel.incrementCounter,
child: Text('Increment'),
),
],
);
}
}

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.

testWidgets('$CounterPage increments counter when button is pressed', (WidgetTester tester) async {
// Arrange and Act
await tester.pumpWidget(const MaterialApp(home: CounterPage()));
// Verify the initial counter value
expect(find.text('Counter: 0'), findsOneWidget);
// Tap the increment button
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// Verify the updated counter value
expect(find.text('Counter: 1'), findsOneWidget);
});

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.

int sum(number, number2) {
return number + number2;
}

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.