Golden Screenshot Testing in Flutter
Golden tests (also called screenshot tests or snapshot tests) capture a widget's rendered output as a PNG and compare it pixel-by-pixel against a stored reference image. They are extraordinarily effective at catching unintended visual regressions — a padding change, a wrong color, a misaligned text node — that functional tests completely miss.
Flutter ships with matchesGoldenFile built in. The ecosystem extends this with golden_toolkit and alchemist, which solve the real-world pain points: font loading, multi-device captures, and managing golden updates on CI. This post covers the full picture.
The Basic Mechanism
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('UserCard matches golden', (tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: UserCard(
name: 'Alice Johnson',
role: 'Senior Engineer',
avatarUrl: null,
),
),
),
);
await expectLater(
find.byType(UserCard),
matchesGoldenFile('goldens/user_card.png'),
);
});
}On first run (or when no golden exists), Flutter writes the current render to goldens/user_card.png. On subsequent runs, it renders the widget fresh and compares it to the stored PNG. Any pixel difference fails the test.
Golden files are checked into source control. They live next to your test files:
test/
widgets/
user_card_test.dart
goldens/
user_card.pngUpdating Goldens
When you intentionally change a widget's appearance, the golden becomes stale. Update it:
flutter test --update-goldensThis regenerates all golden files. Review the diffs in git before committing — the visual diff should match your intent. Many teams use git diff --stat to spot unexpected golden changes in PRs.
To update a single test's golden:
flutter test test/widgets/user_card_test.dart --update-goldensThe Font Problem
The most common frustration with golden tests is font rendering. Flutter test environment does not load fonts by default. Text renders as boxes:
████████ ████ ████████This makes goldens that include any text nearly useless without font loading. Fix it by loading fonts in your test setup:
testWidgets('UserCard with real fonts', (tester) async {
// Load fonts from pubspec.yaml assets
final fonts = await loadAppFonts(); // from golden_toolkit
addTearDown(fonts.dispose);
await tester.pumpWidget(/* ... */);
await expectLater(find.byType(UserCard), matchesGoldenFile('goldens/user_card.png'));
});golden_toolkit provides loadAppFonts() which reads your pubspec.yaml and loads all declared fonts automatically.
golden_toolkit: The Production-Ready Solution
Add golden_toolkit to your dev dependencies:
dev_dependencies:
golden_toolkit: ^0.15.0
flutter_test:
sdk: flutterGoldenToolkit Setup
Create a flutter_test_config.dart at the root of your test/ directory. Flutter test runner auto-executes this file before any test:
// test/flutter_test_config.dart
import 'dart:async';
import 'package:golden_toolkit/golden_toolkit.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
return GoldenToolkit.runWithConfiguration(
testMain,
config: GoldenToolkitConfiguration(
skipGoldenAssertion: () => !Platform.isMacOS, // Only compare on macOS
enableRealShadows: true,
),
);
}The skipGoldenAssertion callback is important: font rendering and anti-aliasing differ between Linux (CI) and macOS (developer machines). More on this below.
loadAppFonts and pumpWidgetBuilder
import 'package:golden_toolkit/golden_toolkit.dart';
void main() {
setUp(() async {
await loadAppFonts();
});
testWidgets('Button golden', (tester) async {
await tester.pumpWidgetBuilder(
const PrimaryButton(label: 'Get Started'),
surfaceSize: const Size(300, 80),
);
await screenMatchesGolden(tester, 'primary_button');
});
}pumpWidgetBuilder wraps your widget in a MaterialApp with proper theme, and crops the golden to the exact widget bounds (using surfaceSize). No need to render the entire screen when you just want the component.
screenMatchesGolden is golden_toolkit's replacement for matchesGoldenFile. It stores goldens in a goldens/ subdirectory automatically and names them after your test.
Multi-Device Goldens
Test the same widget across multiple screen sizes in one shot:
void main() {
setUp(() async => loadAppFonts());
testWidgets('ResponsiveCard on multiple devices', (tester) async {
await tester.pumpWidgetBuilder(const ResponsiveCard(title: 'Hello'));
await multiScreenGolden(
tester,
'responsive_card',
devices: [
Device.phone,
Device.tabletPortrait,
Device.tabletLandscape,
const Device(
name: 'large_phone',
size: Size(428, 926),
devicePixelRatio: 3.0,
),
],
);
});
}This generates separate golden files for each device:
goldens/responsive_card.phone.pnggoldens/responsive_card.tabletPortrait.pnggoldens/responsive_card.tabletLandscape.pnggoldens/responsive_card.large_phone.png
Custom Pump Function
For widgets that need async setup (loading data, animations settling):
testWidgets('AnimatedCounter after animation', (tester) async {
await tester.pumpWidgetBuilder(
const AnimatedCounter(value: 42),
// Custom pump to let animation complete
wrapper: materialAppWrapper(theme: AppTheme.light),
);
// Advance animation
await tester.pump(const Duration(milliseconds: 500));
await screenMatchesGolden(tester, 'animated_counter_final');
});Handling Platform Rendering Differences
The hardest problem in golden testing is that pixel output differs between operating systems. The same widget looks subtly different on macOS vs Linux because:
- Font rasterization differs between CoreText (macOS) and FreeType (Linux).
- Anti-aliasing algorithms differ.
- Shadow rendering can vary.
Strategies for managing this:
Strategy 1: Only Compare on One Platform
The simplest: generate and compare goldens only on macOS (or only on Linux CI). Skip on other platforms:
// In flutter_test_config.dart
config: GoldenToolkitConfiguration(
skipGoldenAssertion: () => !Platform.isMacOS,
),Developers on macOS update goldens locally. CI on Linux skips the pixel comparison (but still renders widgets, catching layout errors).
Strategy 2: Machine-Specific Goldens
Store separate goldens per platform:
String goldenPath(String name) {
if (Platform.isMacOS) return 'goldens/mac/$name.png';
if (Platform.isLinux) return 'goldens/linux/$name.png';
return 'goldens/$name.png';
}
await expectLater(
find.byType(MyWidget),
matchesGoldenFile(goldenPath('my_widget')),
);CI and developer machines maintain separate golden directories. This doubles storage but eliminates false failures.
Strategy 3: Tolerance Thresholds
Accept small pixel differences using a custom comparator. Flutter lets you override the global comparator:
// test/flutter_test_config.dart
void main() {
goldenFileComparator = TolerantComparator(
Uri.parse('test/'),
threshold: 0.005, // 0.5% pixel difference tolerance
);
}Implementing TolerantComparator:
class TolerantComparator extends LocalFileComparator {
final double threshold;
TolerantComparator(Uri testFile, {this.threshold = 0.005})
: super(testFile);
@override
Future<bool> compare(Uint8List imageBytes, Uri golden) async {
final result = await GoldenFileComparator.compareLists(
imageBytes,
await getGoldenBytes(golden),
);
if (!result.passed && result.diffPercent <= threshold) {
// Within tolerance — consider it passing
return true;
}
return result.passed;
}
}Strategy 4: Use alchemist (Canvaskit-based)
The alchemist package renders widgets using the Canvaskit renderer, which produces identical output across platforms. It is the most robust solution for cross-platform consistency:
dev_dependencies:
alchemist: ^0.7.0import 'package:alchemist/alchemist.dart';
void main() {
goldenTest(
'UserCard renders correctly',
fileName: 'user_card',
builder: () => GoldenTestGroup(
children: [
GoldenTestScenario(
name: 'default',
child: const UserCard(name: 'Alice', role: 'Engineer'),
),
GoldenTestScenario(
name: 'long name',
child: const UserCard(
name: 'Alexandria Bartholomew-Whitmore',
role: 'Principal Software Engineer',
),
),
],
),
);
}Alchemist renders all scenarios on a single golden image with labels, making visual review much easier.
What Makes a Good Golden Test
Golden tests are most valuable for:
- Design system components — Buttons, cards, badges, chips. These are shared widely and regressions ripple everywhere.
- Complex layouts — Grid views, data tables, multi-column layouts where spacing bugs are easy to introduce.
- Themed variants — Same widget in light mode, dark mode, high-contrast.
- State variations — Loading, error, empty, populated — each captured separately.
- Typography-heavy widgets — Golden tests catch line height and spacing bugs that functional tests miss.
Avoid golden tests for:
- Highly animated widgets (capture at a specific frame instead).
- Widgets whose content changes frequently (like timestamps).
- Platform-specific UI (date pickers, native switches).
Organizing Golden Files in the Repository
test/
goldens/ ← Generated files, committed to git
mac/ ← macOS-specific (if using per-platform strategy)
user_card.png
primary_button.png
linux/
user_card.png
primary_button.png
widgets/
user_card_test.dart
button_test.dart
flutter_test_config.dartAdd a .gitattributes entry to handle PNG binary diffs cleanly:
test/goldens/**/*.png binaryThis prevents Git from trying to text-diff PNG files.
CI Integration
# .github/workflows/golden_tests.yml
name: Golden Tests
on: [pull_request]
jobs:
golden:
runs-on: ubuntu-latest # or macos-14 if you use macOS goldens
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.0'
- name: Install dependencies
run: flutter pub get
- name: Run golden tests
run: flutter test test/widgets/ --tags golden
- name: Upload golden diff artifacts on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: golden-failures
path: test/failures/ # golden_toolkit writes diffs here
retention-days: 7When a golden test fails, golden_toolkit writes three images to test/failures/:
*_masterImage.png— the stored golden*_testImage.png— what was actually rendered*_isolatedDiff.png— pixels that differ (highlighted)
Uploading these as CI artifacts lets reviewers see exactly what changed without checking out the branch.
Tagging Golden Tests
Tag golden tests so you can run them separately from fast unit tests:
testWidgets(
'UserCard golden',
tags: ['golden'],
(tester) async {
// ...
},
);flutter test --tags golden # Only golden tests
flutter test --exclude-tags golden # Everything except golden testsThis lets CI run fast tests on every push and golden tests only on PRs or scheduled runs.
Conclusion
Golden tests occupy a unique position in the Flutter testing hierarchy: they cannot be replaced by functional tests, and they catch entire categories of visual regression automatically. The setup friction — font loading, platform consistency, CI configuration — is real but manageable with golden_toolkit or alchemist. Start with your design system components (they have the highest regression risk and the most stable expected output), establish a per-platform or tolerance-based comparison strategy, and wire golden failures to upload diff images in CI. Once in place, golden tests silently protect your UI and surface any unintended change before it reaches a user.