code, PRs, and late-night deploys

Back to Learn
Testing Beginner

Golden Testing en Flutter

Guía paso a paso para el workshop

En este workshop vamos a aprender qué son los golden tests, por qué son una herramienta poderosa para validar la UI de nuestras apps Flutter, y cómo implementarlos usando la API nativa de flutter_test, junto con bc_golden_plugin, el paquete open source de Bancolombia que resuelve problemas comunes como fuentes, múltiples dispositivos y consistencia entre entornos.

Cubriremos: creación del proyecto, tu primer golden test, generación y actualización de goldens, pruebas con distintos temas y tamaños de pantalla, manejo de fuentes, golden tests de pantallas completas, integración con bc_golden_plugin, ejecución en CI y buenas prácticas para que no se rompan constantemente.


Tabla de contenidos

  1. ¿Qué es un Golden Test?
  2. Requisitos previos
  3. Crear el proyecto del workshop
  4. Estructura de carpetas para testing
  5. Tu primer Golden Test
  6. Generar y actualizar los goldens
  7. Golden Tests con Material y temas (light/dark)
  8. Testeando distintos tamaños de pantalla
  9. Manejo de fuentes (el problema #1 de los golden tests)
  10. Golden Tests de pantallas completas (Screen Goldens)
  11. Integración con bc_golden_plugin
  12. Ejecutar los tests en CI (GitHub Actions)
  13. Buenas prácticas
  14. Errores comunes y cómo resolverlos
  15. Recursos adicionales

1. ¿Qué es un Golden Test?

Un golden test (también llamado snapshot test) es un tipo de test que renderiza un widget, lo convierte en una imagen (.png), y la compara pixel a pixel contra una imagen de referencia ("golden") guardada previamente en el repositorio.

Si el widget cambia visualmente (un color, un padding, una fuente, un ícono), el test falla y te muestra el diff entre la imagen esperada y la generada. Esto permite detectar regresiones visuales de forma automática, sin depender de revisión manual.

Ventajas:

  • Detecta cambios visuales no intencionados (regresiones de UI).
  • Sirve como documentación viva del diseño de tus componentes.
  • Es rápido: no necesita un dispositivo real ni un emulador corriendo.
  • Se integra perfectamente en pipelines de CI/CD.

Limitaciones:

  • No valida comportamiento/interacción, solo apariencia.
  • Es sensible a la plataforma en la que se generan las imágenes (fuentes, renderizado).
  • Requiere disciplina para mantener los goldens actualizados.

2. Requisitos previos

Antes de empezar, asegúrate de tener:

  • Conocimientos básicos de Dart y Flutter (widgets, StatelessWidget, StatefulWidget).
  • Un editor de código: VS Code o Android Studio.
  • Git instalado (git --version en tu terminal).
  • Flutter instalado y verificado con flutter doctor — si aún no lo tienes, sigue primero la guía Instalación de Flutter.

3. Crear el proyecto del workshop

Crea un nuevo proyecto Flutter:

flutter create golden_testing_workshop
cd golden_testing_workshop

Verifica que corre correctamente:

flutter run

Abre el proyecto en tu editor (ejemplo con VS Code):

code .

Revisa que el archivo pubspec.yaml tenga el paquete de testing incluido por defecto (viene con cualquier proyecto Flutter):

dev_dependencies:
  flutter_test:
    sdk: flutter

4. Estructura de carpetas para testing

Organiza tu proyecto de la siguiente manera para mantener los golden tests ordenados:

golden_testing_workshop/
├── lib/
│   └── widgets/
│       └── custom_button.dart
├── test/
│   ├── widgets/
│   │   └── custom_button_golden_test.dart
│   └── goldens/              # aquí se guardan las imágenes .png de referencia
│       └── custom_button/
│           ├── default.png
│           ├── pressed.png
│           └── disabled.png
└── pubspec.yaml

Tip: algunos equipos prefieren guardar los .png junto al archivo del test (test/widgets/goldens/...). Ambas convenciones son válidas; lo importante es ser consistente en todo el proyecto.


5. Tu primer Golden Test

Vamos a crear un widget sencillo y su golden test.

El widget

lib/widgets/custom_button.dart:

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  const CustomButton({
    super.key,
    required this.label,
    this.onPressed,
  });

  final String label;
  final VoidCallback? onPressed;

  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.deepPurple,
        foregroundColor: Colors.white,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
      ),
      child: Text(label),
    );
  }
}

El golden test

test/widgets/custom_button_golden_test.dart:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_testing_workshop/widgets/custom_button.dart';

void main() {
  testWidgets('CustomButton - estado por defecto', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: Center(
            child: CustomButton(label: 'Enviar'),
          ),
        ),
      ),
    );

    await expectLater(
      find.byType(CustomButton),
      matchesGoldenFile('goldens/custom_button/default.png'),
    );
  });

  testWidgets('CustomButton - deshabilitado', (tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Scaffold(
          body: Center(
            child: CustomButton(label: 'Enviar', onPressed: null),
          ),
        ),
      ),
    );

    await expectLater(
      find.byType(CustomButton),
      matchesGoldenFile('goldens/custom_button/disabled.png'),
    );
  });
}

Puntos clave:

  • matchesGoldenFile compara el widget renderizado contra el archivo indicado.
  • La primera vez que corres el test, el archivo .png no existe, así que debes generarlo (siguiente sección).
  • Usa siempre MaterialApp + Scaffold como contenedor, ya que muchos widgets dependen de un Directionality y un MediaQuery válidos para renderizar correctamente.

6. Generar y actualizar los goldens

Para generar los goldens por primera vez (o actualizarlos cuando el cambio visual es intencional), ejecuta:

flutter test --update-goldens

Esto crea/sobrescribe los archivos .png de referencia según lo que se renderiza actualmente.

Para simplemente correr los tests y validarlos contra los goldens existentes:

flutter test test/widgets/custom_button_golden_test.dart

O correr toda la suite de tests del proyecto:

flutter test

Importante: revisa siempre visualmente los .png generados antes de hacer commit. Un --update-goldens sin revisión puede "aprobar" un bug visual sin darte cuenta.


7. Golden Tests con Material y temas (light/dark)

Es común querer validar un mismo widget en distintos temas. Usa una función helper para no repetir código:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_testing_workshop/widgets/custom_button.dart';

Widget _buildApp(Widget child, {required Brightness brightness}) {
  return MaterialApp(
    theme: ThemeData(brightness: brightness),
    home: Scaffold(
      body: Center(child: child),
    ),
  );
}

void main() {
  testWidgets('CustomButton - tema claro', (tester) async {
    await tester.pumpWidget(
      _buildApp(const CustomButton(label: 'Enviar'), brightness: Brightness.light),
    );

    await expectLater(
      find.byType(CustomButton),
      matchesGoldenFile('goldens/custom_button/light_theme.png'),
    );
  });

  testWidgets('CustomButton - tema oscuro', (tester) async {
    await tester.pumpWidget(
      _buildApp(const CustomButton(label: 'Enviar'), brightness: Brightness.dark),
    );

    await expectLater(
      find.byType(CustomButton),
      matchesGoldenFile('goldens/custom_button/dark_theme.png'),
    );
  });
}

8. Testeando distintos tamaños de pantalla

Puedes controlar el tamaño del "dispositivo virtual" con tester.view (Flutter 3.x+) o binding.window (versiones anteriores):

testWidgets('CustomButton - en pantalla pequeña', (tester) async {
  tester.view.physicalSize = const Size(320, 640);
  tester.view.devicePixelRatio = 1.0;

  addTearDown(tester.view.reset);

  await tester.pumpWidget(
    const MaterialApp(
      home: Scaffold(
        body: Center(child: CustomButton(label: 'Enviar')),
      ),
    ),
  );

  await expectLater(
    find.byType(CustomButton),
    matchesGoldenFile('goldens/custom_button/small_screen.png'),
  );
});

addTearDown(tester.view.reset) es clave: evita que la configuración de tamaño "se filtre" a otros tests que corran después.


9. Manejo de fuentes (el problema #1 de los golden tests)

Por defecto, en el entorno de test, Flutter usa una fuente de prueba (Ahem) en lugar de las fuentes reales del sistema, salvo que se le indique lo contrario. Esto puede causar diferencias entre lo que ves en tu app real y lo que aparece en el golden.

Para cargar las fuentes reales (incluyendo íconos como Material Icons) antes de correr los tests, crea un archivo de configuración:

test/flutter_test_config.dart:

import 'dart:async';
import 'package:bc_golden_plugin/bc_golden_plugin.dart';

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  await loadConfiguration();
  await testMain();
}

Flutter detecta automáticamente flutter_test_config.dart en la carpeta test/ y lo ejecuta antes de cualquier test. loadConfiguration() viene del paquete bc_golden_plugin (ver sección 11) y garantiza que las fuentes se rendericen igual en todas las máquinas.

Importante: los golden tests son sensibles al sistema operativo en el que se generan (antialiasing, subpixel rendering). Se recomienda generar y correr los goldens siempre en el mismo entorno (por ejemplo, dentro de Docker o en el runner de CI) para evitar falsos positivos entre máquinas de distintos desarrolladores.


10. Golden Tests de pantallas completas (Screen Goldens)

Además de widgets individuales, puedes testear pantallas completas:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_testing_workshop/screens/home_screen.dart';

void main() {
  testWidgets('HomeScreen - estado inicial', (tester) async {
    await tester.pumpWidget(const MaterialApp(home: HomeScreen()));
    await tester.pumpAndSettle();

    await expectLater(
      find.byType(HomeScreen),
      matchesGoldenFile('goldens/screens/home_screen.png'),
    );
  });
}

pumpAndSettle() espera a que terminen animaciones y microtasks pendientes (por ejemplo, un FutureBuilder cargando datos) antes de tomar la captura.

Si tu pantalla depende de datos remotos (API, base de datos), usa fakes o mocks para las dependencias, de forma que el test sea determinista y no dependa de la red:

testWidgets('HomeScreen - con datos mockeados', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: HomeScreen(repository: FakeUserRepository()),
    ),
  );
  await tester.pumpAndSettle();

  await expectLater(
    find.byType(HomeScreen),
    matchesGoldenFile('goldens/screens/home_screen_with_data.png'),
  );
});

11. Integración con bc_golden_plugin

El paquete flutter_test incluye matchesGoldenFile de forma nativa, pero para workshops y proyectos reales conviene usar un paquete que resuelva los problemas de fuentes y multi-dispositivo automáticamente. bc_golden_plugin es un paquete open source de Bancolombia, publicado bajo licencia Apache-2.0, que extiende los golden tests nativos con esas soluciones.

Agrégalo como dependencia de desarrollo:

flutter pub add --dev bc_golden_plugin

Con bc_golden_plugin puedes reescribir el test de la sección 5 usando su API unificada BcGoldenCapture:

import 'package:bc_golden_plugin/bc_golden_plugin.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:golden_testing_workshop/widgets/custom_button.dart';

void main() {
  BcGoldenCapture.single(
    'CustomButton - estado por defecto',
    (tester) async {
      await tester.pumpWidget(
        const MaterialApp(
          home: Scaffold(
            body: Center(
              child: CustomButton(label: 'Enviar'),
            ),
          ),
        ),
      );

      await expectLater(
        find.byType(CustomButton),
        matchesGoldenFile('goldens/custom_button/default.png'),
      );
    },
    shouldUseRealShadows: true,
  );
}

BcGoldenCapture.single envuelve testWidgets y aplica automáticamente la configuración de fuentes que viste en la sección 9. También trae dispositivos predefinidos (iPhone 8, iPhone 13, Pixel 5, iPad Pro, entre otros) para no tener que calcular tamaños y devicePixelRatio a mano, y soporte para flujos multi-pantalla y animaciones a través de BcGoldenCapture.multiple y BcGoldenCapture.animation.

Para el workshop, recomendamos empezar con matchesGoldenFile nativo (secciones 5-8) para entender los fundamentos, y luego mostrar bc_golden_plugin como la forma "productiva" de escalar esto a un proyecto real.


12. Ejecutar los tests en CI (GitHub Actions)

Crea .github/workflows/golden_tests.yml:

name: Golden Tests

on:
  pull_request:
    branches: [main]

jobs:
  golden-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          channel: 'stable'
          cache: true

      - name: Instalar dependencias
        run: flutter pub get

      - name: Correr golden tests
        run: flutter test --update-goldens=false

Como se mencionó en la sección 9, corre siempre los goldens en el mismo sistema operativo (por ejemplo, siempre en ubuntu-latest) tanto en CI como al generarlos localmente, usando Docker si es necesario, para evitar falsos positivos por diferencias de renderizado entre macOS/Windows/Linux.


13. Buenas prácticas

  • Un widget, un golden pequeño y enfocado. Evita golden tests gigantes de pantallas completas como primera línea de defensa; combínalos con golden tests de widgets aislados.
  • Nombra los archivos de forma descriptiva: button_disabled.png, no test1.png.
  • Versiona los .png en git (no los ignores en .gitignore); son parte del contrato visual del proyecto.
  • Revisa el diff visual en cada PR. Muchas herramientas de CI (o extensiones de GitHub) permiten ver el .png esperado vs. el generado.
  • No abuses de --update-goldens. Regenerar sin revisar es la forma más común de introducir bugs visuales "aprobados" sin darse cuenta.
  • Fija el devicePixelRatio y el tamaño de pantalla explícitamente en cada test para resultados reproducibles.
  • Aísla dependencias externas (red, fecha/hora, Random) para que el test sea determinista.

14. Errores comunes y cómo resolverlos

Problema Causa probable Solución
Golden file does not exist Es la primera corrida del test Ejecuta flutter test --update-goldens
Los tests pasan en tu máquina pero fallan en CI Diferencias de fuente/render entre SO Usa bc_golden_plugin o corre siempre en el mismo entorno (Docker/CI)
El texto se ve como cuadros o líneas Fuente Ahem por defecto en tests Configura flutter_test_config.dart con loadConfiguration() (sección 9)
Diferencias mínimas de píxeles (antialiasing) Renderizado distinto entre versiones de Flutter/SO Ajusta el umbral de comparación o fija la versión de Flutter en CI
El widget se corta o aparece con tamaño incorrecto Falta MediaQuery/Directionality, o el Scaffold no define tamaño Envuelve el widget en MaterialApp + Scaffold, o fija tester.view.physicalSize
Animaciones incompletas en la captura No se esperó a que terminaran Usa await tester.pumpAndSettle() antes del expectLater

15. Recursos adicionales


thanks for visiting ✦