H.org
  • Questions

Snapshot testing with Flutter Golden

  1. flutter
  2. testing

Golden is a part of the flutter_test package and enables you to compare your widgets against a rasterised master reference image (a golden) in tests. This is particularly handy for catching layout regressions like changes in colour, text size, positioning etc. These tests compare your widget, which is built at runtime from the pumpWidget function, to a golden image snapshot.

The name for this type of testing comes from a "master image" which is a true and accurate representation of something. When games are completed they "go gold", which is in reference to the colour of master archival CDs.

Getting started

You start by creating a regular test case as usual:

void main() {
group('ACME Widget', () {
testWidgets('renders correctly', (WidgetTester tester) async {
...
});
});
}

Next, add the call to pumpWidget with the given Widget you'd like to test, along with the expectLater assertion:

await tester.pumpWidget(Widget());

await expectLater(
find.byType(Widget),
matchesGoldenFile('goldens/widget.png')
);

The filename referenced in the string above can be anything, but it's usually a good practice to store the golden near the widget code itself.

Generating goldens

Once you have a few tests set up and ready to go, you will find that if you run flutter test, your tests will fail. This is because the matcher above is expecting goldens/widget.png to exist and match the pumped widget under test. It's still good to run the tests first just to make sure they're being picked up correctly.

Generate goldens by adding the --update-goldens argument:

flutter test --update-goldens

The test suite will run through all tests and create goldens at the path supplied to matchesGoldenFile().

You will need to repeat this process any time there are changes to the widget. However, don't just blindly update the goldens, always check to make sure the change was intended.

Gotchas

Why is the text all blocky?

The test environment in which goldens are generated loads a specific font called Ahem, which was specifically crafted for testing font and text properties in things like web browsers. Don't panic, your widget hasn't broken itself!

Localisations

If your widget relies on Localization.of(context) to fetch localised strings, the context won't have access to an appropriate delegate to query.

Wrap the widget being tested in a MaterialApp in order to provide the LocalizationDelegate to the context:

MaterialApp(
localizationDelegates: [
const SampleLocalizationDelegate()
],
home: <subtree>
)

Repaint

If you are pumping a large widget tree, you can use RepaintBoundary to prevent everything except the widget itself from being painted into the golden.

Here's an example of a huge unbounded snapshot:

And here it is wrapped in a boundary:

You'll notice there's still a minor amount of space around the button, this is just the button's rendered drop shadow.

Sizing

Because widgets will try and fill all available space, you may find your golden images look strange. The widget for a button might cover the entire canvas, rather than be sized appropriately. This is usually remedied using Center somewhere above the widget under test:

Center(
child: RepaintBoundary(
child: <test widget>,
)
)

Using Center forces the widget to size itself appropriately.

Missing images

When testing a widget that loads an asset such as Image, you will find that they are completely missing in your golden. This is because images in Flutter are loaded asynchronously.

Luckily, precacheImage is provided to load the image asset before the golden is rendered:

test.runAsync(() {
await tester.pumpWidget(ACMELabsLoadingIndicator());

// Find the Image widget. Depending on your widget tree
// this might be simple or a total pain.
var element = tester.element(find.byType(Image));
Image widget = element.widget;

await precacheImage(widget.image);
await tester.pumpAndSettle();
});

await expectLater(
find.byType(ACMELabsLoadingIndicator),
matchesGoldenFile('goldens/acme_labs_loading_indicator.png');
)

Animations

Widgets that contain animations can make a golden test fail. In these scenarios, you should pump the widget for the duration of the animation:

await tester.pumpAndSettle(Duration(seconds: 3));

This ensures the animation is finished and the test is in a reproducible state before checking it against the golden.

Exceptions

Since widget tests should be small and specific, you will invariably come across exceptions when testing your widget in isolation. This is because most widgets rely on specific parent widgets, such as a TextField having a parent TextDirection.

In most cases, this just means wrapping your widget under test in whatever is required:

await tester.pumpWidget(
TextDirection(
child: RepaintBoundary(
child: Widget(),
),
),
);

This should be enough to get you started with widget testing using goldens, and soon enough you will have plenty of regression tests for your UI.

Ko-Fi button Support Me on Ko-fi

Created with lots of green tea and 11ty

Opinions are solely my own and not those of my employer.

Privacy Policy - Licenses