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
- ¿Qué es un Golden Test?
- Requisitos previos
- Crear el proyecto del workshop
- Estructura de carpetas para testing
- Tu primer Golden Test
- Generar y actualizar los goldens
- Golden Tests con Material y temas (light/dark)
- Testeando distintos tamaños de pantalla
- Manejo de fuentes (el problema #1 de los golden tests)
- Golden Tests de pantallas completas (Screen Goldens)
- Integración con bc_golden_plugin
- Ejecutar los tests en CI (GitHub Actions)
- Buenas prácticas
- Errores comunes y cómo resolverlos
- 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 --versionen 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_workshopVerifica que corre correctamente:
flutter runAbre 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: flutter4. 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.yamlTip: algunos equipos prefieren guardar los
.pngjunto 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:
matchesGoldenFilecompara el widget renderizado contra el archivo indicado.- La primera vez que corres el test, el archivo
.pngno existe, así que debes generarlo (siguiente sección). - Usa siempre
MaterialApp+Scaffoldcomo contenedor, ya que muchos widgets dependen de unDirectionalityy unMediaQueryvá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-goldensEsto 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.dartO correr toda la suite de tests del proyecto:
flutter testImportante: revisa siempre visualmente los
.pnggenerados antes de hacer commit. Un--update-goldenssin 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_pluginCon 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
matchesGoldenFilenativo (secciones 5-8) para entender los fundamentos, y luego mostrarbc_golden_plugincomo 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=falseComo 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, notest1.png. - Versiona los
.pngen 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
.pngesperado 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
devicePixelRatioy 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
- Documentación oficial de Flutter: testing
bc_golden_pluginen pub.dev- Repositorio en GitHub: bancolombia/bc_golden_plugin
- API de
matchesGoldenFile - Flutter Cookbook: testing
- Instalación de Flutter — guía separada de instalación y configuración del SDK