La última suite de goldens que heredamos tenía 214 imágenes. Todo en verde, durante unos ocho meses, a través de varios cientos de pull requests. Hasta que alguien abrió uno de los PNG para ver qué protegía.
Cada glifo era un rectángulo negro.
La suite se había escrito copiando el primer tutorial que apareció, y eso no es una crítica: el tutorial funciona, en el sentido de que flutter test --update-goldens produce archivos y la siguiente ejecución los compara. Solo que compara lo que no es: una maquetación de cajas rellenas, con las sombras aplanadas en bloques duros, en una máquina que nadie había fijado. Cambia una palabra por otra de la misma longitud, cambia la familia tipográfica, y los píxeles no se mueven. La suite no estaba midiendo la interfaz. Estaba midiendo que nada se había movido.
Ese es el problema completo de los golden tests en un ejemplo: son a la vez la red de regresión más barata de Flutter y la falsificación más convincente. Una marca verde se ve idéntica en los dos casos.
Lo esencial
- Un golden es una instantánea del árbol de render a un tamaño lógico, un tema, un idioma y una escala de texto — y nada más. No es una captura de tu app en un teléfono.
- El entorno de test no trae las fuentes de la app. El texto sin familia disponible se dibuja como cajas sólidas, a propósito, para que la maquetación sea determinista. Sin cargar fuentes estás haciendo golden tests de geometría y llamándolo test visual.
- Las sombras se aplanan en bloques duros (
debugDisableShadows): el framework descarta el desenfoque. La elevación sigue en la imagen, pero el radio de desenfoque, la propagación y el degradado no — así que las regresiones de sombra son invisibles. - Los píxeles dependen del motor, del sistema anfitrión y de la versión de Flutter. Una suite generada en macOS falla en CI Linux. Fija el toolchain y genera en la misma plataforma donde corre CI.
tester.viewes la superficie, no el dispositivo. La superficie por defecto son 800×600 píxeles lógicos con una relación de píxeles de 3; sin fijar un tamaño, cada pantalla de móvil se renderiza a una resolución que ningún usuario tiene.pumpAndSettlese cuelga con un spinner — un indicador de progreso indeterminado programa fotogramas para siempre. Los estados de carga necesitanpump(duration).- La matriz se multiplica. 12 pantallas × 2 temas × 3 anchos × 2 escalas de texto × 2 idiomas son 288 imágenes, todas regeneradas en cada actualización de Flutter. Elige los ejes que de verdad vas a revisar.
- El valor de una suite de goldens es la probabilidad de que alguien mire el diff. Todo lo demás es fontanería. Un commit con
--update-goldensque nadie revisó convierte un test en un archivo de bloqueo.
Qué hace realmente un golden test
Un test, una imagen, una comparación:
testWidgets('cart is empty', (tester) async {
await pumpScreen(tester, const CartScreen(items: []));
await expectLater(
find.byType(CartScreen),
matchesGoldenFile('goldens/cart_empty.light.390.ts1.0.en.png'),
);
});
flutter test --update-goldens escribe ese PNG y pasa. Cada ejecución posterior lo decodifica y lo compara píxel a píxel con lo que acaba de renderizar el motor. Sin tolerancia ni hash perceptual: igualdad exacta, hasta que instales un goldenFileComparator propio. Cuando falla, el comparador por defecto deja cuatro artefactos en un directorio failures/ junto al archivo de test: la referencia, el render nuevo, un diff aislado y un diff enmascarado. (Si los tamaños no coinciden escribe solo los dos primeros.) Esos son tu revisión del diff, y su sitio es .gitignore.
Dos detalles de ese fragmento importan más de lo que parece.
find.byType(CartScreen) no captura CartScreen. El matcher sube desde el elemento encontrado hasta el ancestro RepaintBoundary más cercano y captura esa capa. Normalmente ese ancestro es la app entera, y por eso los goldens salen tantas veces como una imagen de toda la ventana con el banner de debug en una esquina. Envuelve el sujeto en un RepaintBoundary y capturarás exactamente lo que querías.
La imagen renderizada no es tu app. Es flutter_tester en tu máquina anfitriona: sin fuentes del sistema, sin interfaz del sistema, sin teclado, con una red simulada que solo devuelve 400, con las sombras reducidas a bloques duros y en la superficie que le tocara al test. Un golden que pasa te dice que este árbol, con este tamaño y estas entradas, no ha cambiado. Eso vale mucho — y es mucho más estrecho que «la interfaz funciona».
Las cinco razones por las que tu primer golden te miente
1. No hay fuentes, así que cada glifo es una caja
flutter test se ejecuta sin las fuentes declaradas en pubspec.yaml. El texto sin familia disponible cae en una fuente de test cuyos glifos son todos rectángulos rellenos: un diseño que hace la maquetación determinista y prescinde de archivos de fuentes, y que además significa que el PNG de tu repositorio no contiene ni una letra legible.
La solución es test/flutter_test_config.dart, que Flutter ejecuta antes de las suites de ese directorio. Todo lo que declares ahí — carga de fuentes, un comparador propio, configuración del reloj — se aplica a cada test que haya debajo:
// test/flutter_test_config.dart
import 'dart:async';
import 'package:flutter/services.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await _loadFonts();
await testMain();
}
Future<void> _loadFonts() async {
// The family name must match what the theme asks for. If `fontFamily` is
// null, register the same faces as 'Roboto' — that is what Material falls
// back to, and a mismatch loads nothing and fails silently.
// Both faces are declared under `assets:` in pubspec.yaml, and `flutter test`
// can load them from there — a pure `dart test` cannot.
final text = FontLoader('Roboto')
..addFont(rootBundle.load('assets/fonts/Roboto-Regular.ttf'))
..addFont(rootBundle.load('assets/fonts/Roboto-Medium.ttf'));
// Icons are fonts too. Skip this and every `Icon` in every golden is a box.
final icons = FontLoader('MaterialIcons')
..addFont(rootBundle.load('assets/fonts/MaterialIcons-Regular.otf'));
await Future.wait([text.load(), icons.load()]);
}
Dos cosas fallan aquí en silencio en vez de ruidosamente. Un nombre de familia equivocado carga una fuente que ningún widget pide, así que no cambia nada. Y google_fonts descarga sus tipos en tiempo de ejecución, cosa que el entorno de test no hace: guarda en el repositorio los .ttf que realmente distribuyes o tus goldens se quedarán esperando para siempre una llamada de red que nunca ocurre.
Hay una consecuencia más sutil, y es la que golpea a los equipos que se saltan esta sección: con glifos de caja no puedes ver qué dice el texto, y desaparece toda propiedad que la fuente de test normaliza. La familia, el peso, el kerning, la forma del glifo y la ascensión y descendencia reales de la fuente se sustituyen por cuadrados idénticos, así que cambiar de Roboto a Inter dibuja las mismas cajas, y un cambio de copy con el mismo número de caracteres no mueve nada. El número de caracteres y el tamaño de fuente sí mueven las cajas — una cadena rusa más larga produce una barra más ancha —, así que el desbordamiento y los cambios de tamaño no son invisibles. Solo lo son las cosas que hacen legible la tipografía en producción.
2. Las sombras se aplanan, así que el desenfoque no existe
debugDisableShadows no elimina las sombras: las sustituye por bloques sólidos. BoxShadow.toPaint() descarta su máscara de desenfoque, y una superficie Material pinta su elevación como un contorno duro (strokeWidth: elevation * 2), así que en el golden queda una forma sin desenfoque en el color de la sombra. El binding de test lo activa porque el renderizado de sombras no está garantizado como idéntico píxel a píxel entre versiones, ni siquiera entre ejecuciones.
La consecuencia es más estrecha que «sin sombras» y más engañosa: la tarjeta sigue ahí, pero su radio de desenfoque, su propagación y su degradado no, así que una regresión de sombra — un desenfoque que se duplicó, una propagación que rompió la maquetación — se renderiza idéntica en el golden. Los propios tests del framework de Flutter ponen debugDisableShadows = false cuando prueban sombras concretamente, y lo restauran al final del test; el binding comprueba que el flag ha vuelto a su valor por defecto (true), con el que trabaja el resto de la suite. Ese es el planteamiento correcto: devuelve las sombras reales solo donde la sombra es lo que se está probando, acepta allí la inestabilidad y no toques el flag en ningún otro sitio.
3. Toda imagen de red es un placeholder
Image.network en un test llama a un HttpClient simulado que devuelve un 400 vacío. Lo que pasa después depende del widget: sin errorBuilder, el widget Image relanza el error de carga en debug y el test falla con NetworkImageLoadException; con uno, el golden registra tu placeholder en silencio. cached_network_image y compañía necesitan su propio doble. O envuelves las llamadas en HttpOverrides (los tutoriales antiguos lo hacen así), o haces lo que sobrevive a un refactor: recibir un ImageProvider en el widget y pasarle en el test un MemoryImage de un PNG de 1×1.
Los avatares y las fotos de producto son casi todo el peso visual de una pantalla real, así que una suite donde todos son placeholders está probando la maquetación alrededor de las imágenes y nada de las imágenes.
4. El reloj, el idioma y el azar no son tuyos
DateTime.now() dentro de un widget, el DateFormat de intl sin una fecha fija, un Random sin semilla, un uuid generado durante el build: cada uno produce un golden que pasa hoy y falla el segundo martes de algún mes, o que falla solo en CI, donde el idioma de la máquina es otro.
La solución no está en el test sino en el widget: recibe una función now o un Clock, recibe un Locale, recibe una semilla. Merece la pena hacerlo igualmente, porque un widget que lee el reloj del sistema directamente es un widget que no puedes testear en ninguna capa, ni golden ni de otro tipo. Declara Intl.defaultLocale explícitamente en flutter_test_config.dart y eliminarás toda una clase de fallos del tipo «en mi máquina funciona, en CI sale otro idioma»; para fechas que no sean en inglés, llama también a initializeDateFormatting(locale), porque DateFormat lanza una excepción para un idioma del que no tiene datos.
5. pumpAndSettle no vuelve nunca en un estado de carga
pumpAndSettle avanza fotogramas hasta que no queda ninguno programado. Un CircularProgressIndicator indeterminado programa fotogramas mientras gira, así que la llamada agota el tiempo con el famoso «pumpAndSettle timed out», y el golden del estado de carga — el estado que QA nunca mira porque no hay manera de mantener la app en él — resulta ser el que no puedes escribir.
// Not this: it waits for an animation that never ends.
// await tester.pumpAndSettle();
await pumpScreen(tester, const CartScreen.loading());
await tester.pump(const Duration(milliseconds: 100)); // one fixed phase of the spinner
await expectLater(find.byType(CartScreen), matchesGoldenFile('goldens/cart_loading.light.390.ts1.0.en.png'));
Ahora el golden captura el spinner a 100 ms, de forma consistente, para siempre. Está bien: lo que pruebas es que la maquetación de alrededor aguanta mientras algo indeterminado gira, no el spinner en sí.
Una suite verde con 214 archivos no es una suite sólida
Todo lo anterior produce el mismo síntoma: tests que pasan comprobando menos de lo que crees. Nada de eso aparece como fallo, como aviso ni como caída de cobertura. Una suite de goldens es particular en que su modo de fallo es el silencio, así que la única forma de confiar en ella es abrir un PNG y comprobar que contiene lo que crees que contiene. Hazlo una vez, el primer día, para cada tipo distinto de captura de la suite. Son diez minutos y son los diez minutos más rentables de toda la configuración.
La configuración que hace reproducibles los goldens
Cuatro decisiones, todas aburridas, todas se toman una vez:
Fija el toolchain. Un .fvmrc en el repositorio, y que CI lea el mismo archivo. Actualizar Flutter cambia el renderizado de texto, los temas por defecto, los renombrados de CardTheme a CardThemeData y el pintado de sombras, así que todos los goldens del repositorio cambian a la vez. Eso no es una razón para no actualizar; es una razón para que la actualización sea un commit deliberado que no contiene nada más que los goldens regenerados, y que lo dice en el mensaje.
Genera en la plataforma donde corre CI. El antialiasing y el hinting del texto difieren entre macOS y Linux: el mismo widget al mismo tamaño produce píxeles distintos. Una suite generada en el Mac de un desarrollador fallará en un runner Linux, y el «arreglo» habitual es un --update-goldens donde no debería estar. Ejecuta también los goldens en Linux en local, en el mismo contenedor que CI si el equipo es mixto.
No dejes nunca que CI regenere. La protección es una línea:
- uses: subosito/flutter-action@v2
with:
flutter-version-file: .fvmrc # one version, repo and CI
cache: true
- run: flutter test
# A "helpful" --update-goldens in CI or a stale golden written by a bad merge
# would make the suite green while comparing nothing. Fail instead. `git diff`
# alone misses untracked files, which is exactly what a regenerated golden is.
- run: test -z "$(git status --porcelain -- '*.png')"
Esa última línea no es paranoia. La forma más común de que una suite de goldens deje de comprobar algo es un paso de CI añadido a las dos de la mañana durante un build en rojo, que actualiza los goldens en vez de fallar.
Fija la superficie explícitamente. La superficie por defecto son 800×600 píxeles lógicos con relación de píxeles 3. Prueba en ella una pantalla de móvil y obtendrás la maquetación de una tableta pequeña, con el MediaQuery de la app insistiendo en lo contrario. Un solo helper lo resuelve para todos los tests, y si no puedes leer el nombre del golden y saber en qué cinco ejes se renderizó, el nombre está mal:
// test/helpers/golden.dart
Future<void> pumpScreen(
WidgetTester tester,
Widget child, {
Size size = const Size(390, 844), // iPhone 14 — pick your own baselines
double textScale = 1,
Locale locale = const Locale('en'),
Brightness brightness = Brightness.light,
}) async {
tester.view.physicalSize = size * 3; // logical size × device pixel ratio
tester.view.devicePixelRatio = 3;
addTearDown(tester.view.reset); // never leak a view into the next test
await tester.pumpWidget(
MaterialApp(
debugShowCheckedModeBanner: false,
theme: brightness == Brightness.dark ? AppTheme.dark : AppTheme.light,
locale: locale,
// Without the app's own delegates a locale it does not support resolves
// back to English.
localizationsDelegates: AppLocalizations.delegates,
supportedLocales: AppLocalizations.supportedLocales,
home: RepaintBoundary(
child: Builder(
builder: (context) => MediaQuery(
// Merge, don't replace: a fresh MediaQueryData has size zero,
// devicePixelRatio 1 and no padding.
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(textScale),
),
child: child,
),
),
),
),
);
await tester.pump();
}
Dos detalles son fáciles de estropear. MediaQueryData(...) en vez de copyWith construye un objeto nuevo con size: Size.zero y devicePixelRatio: 1, así que MediaQuery.sizeOf miente a todo lo que haya debajo. Y un MaterialApp reconstruido dentro del helper resuelve cualquier idioma que no soporte de vuelta al inglés salvo que le pases los delegados de la app (que es lo que aquí representa AppLocalizations); si no, el eje de idioma prueba inglés en silencio.
addTearDown(tester.view.reset) no es opcional. tester.view es global para el archivo de test, así que un test que pone un ancho de 320pt se filtra a todos los siguientes, y el fallo aparece atribuido a un test inocente posterior dentro del mismo archivo.
Qué convertir en golden y cuánto cuesta la matriz
La trampa es que la matriz parece gratis. No lo es: cada imagen es un archivo que regenerar, un archivo que revisar y un archivo que añadirá una línea al diff de tu siguiente pull request no relacionado.
| Capa | Qué cubre | Imágenes |
|---|---|---|
| Componentes del sistema de diseño — botón, campo, badge, tarjeta, list tile | Cada estado × claro/oscuro, a un ancho | 20–40 |
| Las cinco pantallas que importan | 3 anchos (320/390/768) × claro/oscuro | 30 |
| Estados vacíos, de error y de carga | Los estados que nadie abre la app para ver | 6–10 |
| Escala de texto 2.0, oscuro, 320pt | Los dos componentes más densos del sistema de diseño | 4 |
| Idioma | El idioma con cadenas más largas, un ancho, las dos pantallas con más riesgo de desbordarse | 4 |
Eso es una suite de unas 60–90 imágenes: unos veinte segundos de CI y un pull request cuyo diff visual una persona puede realmente mirar. La versión que lo multiplica todo por todos los ejes — 12 pantallas × 2 temas × 3 anchos × 2 escalas × 2 idiomas — son 288 imágenes, y nadie revisa 288 imágenes.
Hay tres cosas que no vale la pena convertir en golden:
- Animaciones y transiciones. Un golden captura un fotograma en un instante fijo. Te dice que el fotograma de los 100 ms se ve igual, no que la animación sea fluida, y el valor marginal sobre los estados inicial y final es casi cero.
- Cualquier cosa con contenido real dentro. Nombres de producto largos, precios, fechas, IDs generados: cada uno es una razón para que el golden cambie por un motivo no visual. Haz golden del esqueleto con fixtures fijas y prueba el renderizado de contenido con un widget test que afirme sobre el texto, no sobre píxeles.
- Flujos completos. Tres pantallas en una imagen se revisan tres veces peor que una pantalla por imagen.
Y lo más revelador que se puede convertir en golden, en nuestra experiencia, no es una pantalla: es el sistema de diseño a 320pt, en modo oscuro, con el texto al 200 %. Esa combinación es donde se rompen las apps reales, es casi imposible de comprobar a mano en todos los componentes, y es exactamente para lo que sirve un golden.
Lo que los goldens no comprueban
Un golden es una imagen. No sabe nada de significado, y conviene corregir explícitamente unos cuantos supuestos habituales.
Comportamiento. Un botón conectado al callback equivocado se dibuja idéntico. También un formulario que ha dejado de validar, una lista que perdió su controlador de scroll, un diálogo que no se cierra. Eso son widget tests — tap, pump, comprobar qué pasó — y ninguna cantidad de píxeles los sustituye.
Accesibilidad. El árbol de semántica es invisible para una captura: etiquetas que faltan, roles equivocados, un área táctil reducida a 24pt, contraste por debajo de 4.5:1 para texto de cuerpo (3:1 para texto grande). Flutter trae las comprobaciones y son baratas:
testWidgets('cart meets a11y guidelines', (tester) async {
final semantics = tester.ensureSemantics();
addTearDown(semantics.dispose);
await pumpScreen(tester, const CartScreen(items: []));
// Tap targets (48dp Android / 44pt iOS) and text contrast.
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
// The label and role the golden cannot see. `isSemantics` checks only the
// values you pass; `matchesSemantics` would also require every other flag
// to be false, and a real Material button sets `hasEnabledState`.
// (`containsSemantics` is the same matcher on Flutter before 3.40.)
expect(
tester.getSemantics(find.byType(CheckoutButton)),
isSemantics(label: 'Checkout', isButton: true, hasTapAction: true),
);
});
Renderizado en dispositivo real. Los goldens corren sobre el motor anfitrión, así que no te dicen qué hace Impeller en iOS con el mismo árbol ni cómo difiere el shaping de texto del sistema. Si esa diferencia importa, es integration_test en un dispositivo con una captura tomada allí: más lento, más inestable y lo único que responde a la pregunta.
Jank. El presupuesto de fotograma se mide en un build de perfil sobre un dispositivo real, no se deduce de un PNG.
Las reglas de las que depende que haya valido la pena
El valor de la suite es proporcional a la probabilidad de que un ser humano mire el diff. Ese es todo el retorno de la inversión, y es una propiedad cultural, no técnica.
Así que, en concreto:
--update-goldenssolo en un commit cuyo mensaje nombre el cambio visual. «Update goldens» como mensaje significa que el diff no se revisó. «Update goldens: el botón de pago mide 48pt y el estado de error usa la nueva paleta» significa que sí.- Un diff de golden en una pantalla que no tocaste es un hallazgo, no ruido. Es la suite haciendo su trabajo. Agruparlo en un «chore: update goldens» es apagar la suite.
- Si quien revisa no puede ver las imágenes, la fontanería falló. Sube los artefactos de
failures/desde CI, o adjunta el diff a la pull request. Una revisión que consiste en «créeme, los píxeles cambiaron» no es una revisión. - Borra los goldens que aprobarías sin mirar. Una suite de cuarenta imágenes que se revisan todas gana a cuatrocientas que se aprueban en un clic. La métrica no es la cobertura; es la cobertura revisada.
- Cuando los goldens cambian en pull requests no relacionados, para y busca la causa: una versión de Flutter sin fijar, una fuente que no se carga, un
DateTime.now()que llegó a un widget. La inestabilidad de los goldens casi nunca es del mecanismo; es un no determinismo real que ahora conoces.
La checklist de la suite de goldens
De un repositorio vacío a una suite fiable, en un día o dos.
Hacer honesto el renderizado
- Fuentes de la app cargadas en test/flutter_test_config.dart, con el nombre de familia que el tema pide de verdad
- La fuente de iconos cargada también: si no, cada Icon es una caja
- Imágenes de red sustituidas por fixtures en memoria
- Reloj, idioma y semillas inyectados en vez de leídos del entorno
- Estados de carga renderizados con pump(duration) y no con pumpAndSettle
Hacerlo reproducible
- Versión de Flutter fijada en el repositorio y leída por CI
- Goldens generados en Linux, la misma plataforma donde corre CI
- Un paso de CI que falla si algún golden está modificado o sin seguimiento, para que nada se regenere en silencio
- failures/ ignorado en git y subido como artefacto de build para la revisión
- La superficie de test fijada explícitamente: tamaño, relación de píxeles, escala de texto, idioma
Hacerlo revisable
- Cada golden envuelto en un RepaintBoundary para capturar el widget que se pretende
- Los cinco ejes codificados en el nombre del archivo: sujeto, tema, ancho, escala de texto, idioma
- Menos de ~90 imágenes, o una razón declarada para más
- Una regla de que las actualizaciones de goldens son un commit propio con una razón visual declarada
- Alguien ha abierto al menos un PNG de cada tipo y ha confirmado que contiene texto
Lo que cuesta a los seis meses
Montar la suite es un día, quizá dos, la mayor parte en fuentes y fontanería de CI. Escribir cada golden son minutos. El coste recurrente es el que nadie calcula: cada actualización de Flutter regenera cada imagen. Con la matriz de arriba son 60–90 PNG en un commit, dos a cuatro veces al año, cada uno con su propia justificación. Si no puedes permitirte esa revisión, no puedes permitirte la suite, porque una regeneración sin revisar es indistinguible de no tener test, solo que tarda más.
A cambio obtienes las regresiones que ningún otro tipo de test atrapa: el estado vacío que perdió su padding en modo oscuro, la traducción rusa que se salió de un botón de 320pt, el sistema de diseño derivando porque alguien cambió un TextStyle en el tema. Y el caso que nos hizo prestar atención desde el principio: un agente o un compañero cambiando un componente compartido y siendo sinceramente incapaz de ver las cuatro pantallas que rompió. Un golden es lo más parecido que tiene Flutter a darle ojos a un cambio de código, que es exactamente el argumento que hicimos en por qué los agentes de IA se atascan con Flutter.
La versión corta
Un golden test es una comparación de píxeles contra un PNG, y todo lo que lo hace útil tiene que ver con las condiciones de esa comparación. Carga tus fuentes, carga tu fuente de iconos, fija la superficie, fija el toolchain, regenera en la plataforma que usa CI y no dejes nunca que una máquina apruebe el diff.
Y luego mantén la suite lo bastante pequeña como para que una persona siga mirándola. El modo de fallo de los golden tests no es un test inestable: son 214 imágenes que pasan todas porque comparan cajas con cajas, y una marca verde que lleva ocho meses contando la misma historia cómoda.
Fuentes
matchesGoldenFile— qué captura el matcher y dónde se enganchagoldenFileComparator: api.flutter.devFontLoader— registrar una fuente por nombre de familia dentro de un test: api.flutter.devdebugDisableShadows— declarado enpainting/debug.dart, activado por el binding de test y reactivado por los propios tests de sombras de Flutter: github.com/flutter/flutterTestFlutterView—physicalSize,devicePixelRatioyreset(): api.flutter.devWidgetTester.pumpAndSettle— «repeatedly calls pump with the given duration until there are no longer any frames scheduled»: api.flutter.devAccessibilityGuideline—androidTapTargetGuideline,iOSTapTargetGuideline,textContrastGuideline: api.flutter.dev- Goldens del framework triados en Skia Gold, y el repositorio
flutter/goldenscon los que no pueden usarlo: skia.org, github.com/flutter/goldens alchemist— carpetas de goldens por plataforma, goldens agrupados de varios widgets, configuración consciente de CI: pub.devgolden_screenshot— tests de captura con previsualizaciones de dispositivo y goldens por plataforma: pub.devgolden_toolkit— el paquete que aún recomiendan casi todos los tutoriales de golden tests; descontinuado, última versión en febrero de 2023: pub.devspot— widget tests tipo golden que comparan un árbol de widgets serializado en vez de píxeles, cuando la inestabilidad de píxeles es el problema mayor: pub.devintegration_test— capturas tomadas en un dispositivo real (llama antes aconvertFlutterSurfaceToImage()en Android), la única respuesta a «cómo se ve en un teléfono»: integration_test.dart
Preguntas frecuentes
Si estás montando la capa de tests desde cero, las decisiones que la rodean están en nuestro repaso del proceso de desarrollo de una app Flutter, y el problema de modelo de estado que impide testear media interfaz está en cinco estados que no pueden existir. El tema es el mismo en los tres: una comprobación que pasa no es lo mismo que una comprobación que mira.
Maxim es ingeniero de software senior en Nerdy Production, una agencia Flutter-first que construye y mantiene apps en fintech, salud y retail. También hacemos desarrollo de apps en Flutter para equipos que prefieren saber qué compara su suite de tests antes de la próxima release y no después.

