Snapshot testing with Flutter Golden
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.