Using Golden Tests for UI Testing in Flutter

In software development, UI testing ensures that the user interface behaves and renders correctly. Flutter, Google’s UI toolkit, offers a rich set of testing tools. Among these tools, golden tests (also known as snapshot tests) are invaluable for verifying the visual aspects of your application. This post will guide you through the process of using golden tests for UI testing in Flutter.

What are Golden Tests?

Golden tests, or snapshot tests, capture a ‘golden’ image or output of a UI component. Subsequent test runs compare the current output against this golden image. If there’s a mismatch, the test fails, indicating a visual change that needs review. These tests are crucial for catching unintended visual regressions.

Why Use Golden Tests?

  • Visual Regression Detection: Quickly identify unintended changes to the UI.
  • Consistency: Ensure that UI elements look the same across different devices and platforms.
  • Efficiency: Test complex UI components with minimal code.
  • Confidence: Gain confidence that UI updates don’t introduce visual bugs.

Setting Up Golden Tests in Flutter

Step 1: Add Dependencies

First, you need to add the necessary dependencies to your pubspec.yaml file:

dev_dependencies:
  flutter_test:
    sdk: flutter
  golden_toolkit: ^0.16.0  # Use the latest version

flutter:
  generate: true

After adding the dependencies, run flutter pub get to install them.

Step 2: Configure Golden Test Assets

Make sure you have enabled the generation of generated plugins. Usually it can be achieved adding
flutter:
generate: true
to your pubspec.yaml file. If that’s not the case, add the generation step manually (e.g.
when you don’t use plugins):

flutter gen-l10n

Step 3: Create Your First Golden Test

Now, let’s create a simple UI component and a golden test for it.

Example UI Component
import 'package:flutter/material.dart';

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blue,
      child: const Text(
        'Hello, Golden Test!',
        style: TextStyle(color: Colors.white),
      ),
    );
  }
}
Golden Test Implementation
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:your_app_name/my_widget.dart'; // Replace with your actual path

void main() {
  testGoldens('MyWidget should look correct', (tester) async {
    await loadAppFonts();
    final builder = DeviceBuilder()
      ..overrideDevices((DeviceBuilderCallback deviceBuilder) => [
            deviceBuilder.device.copyWith(name: 'phone', size: Size(400, 200))
          ])
      ..addScenario(
        widget: MyWidget(),
        name: 'default',
      );
    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'my_widget');
  });
}

In this example:

  • We import the necessary packages, including flutter_test and golden_toolkit.
  • The testGoldens function is used to define the golden test.
  • loadAppFonts() ensures that fonts are loaded before the test runs.
  • DeviceBuilder helps define different device screen sizes to run the golden test against.
  • We create an instance of MyWidget and pass it to the DeviceBuilder‘s addScenario
    method.
  • screenMatchesGolden compares the rendered widget against the golden image named my_widget.

Step 4: Run the Golden Test

To run the golden test, use the following command:

flutter test --update-goldens

The --update-goldens flag tells Flutter to generate or update the golden images. The first time you run this command, it will create the golden image in the test/goldens directory.

Step 5: Verify and Maintain Golden Tests

When the test fails, it means the current UI doesn’t match the golden image. Review the changes carefully to determine if the mismatch is intentional or due to a bug.

  • Intentional Changes: If the changes are intentional, update the golden image by running flutter test --update-goldens.
  • Unintentional Changes: If the changes are unintentional, investigate and fix the bug.

Advanced Golden Testing Techniques

1. Testing Different Themes and Locales

Golden tests can be extended to test UI components with different themes and locales.

testGoldens('MyWidget with dark theme should look correct', (tester) async {
  await loadAppFonts();
  final builder = DeviceBuilder()
    ..overrideDevices((DeviceBuilderCallback deviceBuilder) => [
          deviceBuilder.device.copyWith(name: 'phone', size: Size(400, 200))
        ])
    ..addScenario(
      widget: MaterialApp(
        theme: ThemeData.dark(),
        home: MyWidget(),
      ),
      name: 'dark_theme',
    );
  await tester.pumpDeviceBuilder(builder);
  await screenMatchesGolden(tester, 'my_widget_dark');
});

2. Testing on Different Devices

You can define multiple device configurations using the golden_toolkit‘s DeviceBuilder. It provides a device definition with predefined device types.

import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testGoldens('MyWidget should look correct on multiple devices', (tester) async {
    await loadAppFonts();
    final builder = DeviceBuilder()
      ..addScenario(
        widget: MyWidget(),
        name: 'default',
      );

    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'my_widget_multi_device');
  });
}

3. Handling Dynamic Data

When dealing with dynamic data, ensure that your tests are stable by mocking or stubbing the data sources. Use predictable, consistent data for your golden tests.

class MockDataProvider {
  String getData() {
    return 'Consistent Data';
  }
}

class MyWidgetWithData extends StatelessWidget {
  final MockDataProvider dataProvider;

  MyWidgetWithData(this.dataProvider);

  @override
  Widget build(BuildContext context) {
    return Text(dataProvider.getData());
  }
}

void main() {
  testGoldens('MyWidgetWithData should look correct with mocked data', (tester) async {
    await loadAppFonts();
    final builder = DeviceBuilder()
      ..overrideDevices((DeviceBuilderCallback deviceBuilder) => [
            deviceBuilder.device.copyWith(name: 'phone', size: Size(400, 200))
          ])
      ..addScenario(
        widget: MyWidgetWithData(MockDataProvider()),
        name: 'mocked_data',
      );
    await tester.pumpDeviceBuilder(builder);
    await screenMatchesGolden(tester, 'my_widget_with_data');
  });
}

Best Practices for Golden Tests

  • Keep Tests Focused: Each test should focus on a single UI component or interaction.
  • Automate Updates: Integrate golden test updates into your CI/CD pipeline.
  • Review Changes: Always review visual changes when a golden test fails.
  • Use Version Control: Store golden images in your version control system to track changes.
  • Descriptive Names: Use descriptive names for your golden images to make it easier to understand what each test verifies.

Conclusion

Golden tests are a powerful tool for UI testing in Flutter, providing a reliable way to detect visual regressions and ensure consistency. By setting up and maintaining golden tests, you can increase the confidence in your UI and deliver a high-quality user experience. With the golden_toolkit package, creating and managing golden tests becomes straightforward, allowing you to focus on building great Flutter applications.