Kodokon kodokon.com

Tests: tests unitarios de Dart y tests de widgets

Testea la lógica y los widgets con fakes escritos a mano, un reloj simulado y los reflejos correctos de pump.

11 min · 3 preguntas

Abrir esta lección en Kodokon

La pirámide de tests de Flutter: una amplia base de tests unitarios en Dart puro, una capa de tests de widgets, unos pocos tests de integración. Un test unitario nunca debería importar material.dart: si tu lógica lo necesita, el problema es de arquitectura, no técnico. Para aislar el código de sus dependencias, prefiere los fakes escritos a mano frente a los mocks generados: un fake es una implementación simplificada pero realmente funcional del contrato (repository en memoria, reloj controlable); sobrevive a las refactorizaciones y se lee como código normal. Los mocks, que verifican interacciones ("este método se llamó dos veces"), acoplan el test a los detalles de implementación: resérvalos para protocolos donde la interacción es el comportamiento.

DART
import 'package:flutter/foundation.dart';

abstract interface class ScoreRepository {
  Future<int> fetchBest();
}

class ScoreController extends ChangeNotifier {
  ScoreController(this._repository);

  final ScoreRepository _repository;

  int? best;
  Object? error;

  Future<void> load() async {
    try {
      best = await _repository.fetchBest();
      error = null;
    } on Exception catch (e) {
      error = e;
    }
    notifyListeners();
  }
}
El código bajo prueba: sin ningún import de material.dart.

setUp recrea los objetos antes de cada test: sin estado compartido, así que ningún test depende del orden de ejecución, esa es la condición de su fiabilidad. Los matchers componibles (isA<Exception>(), throwsA, isNull) producen mensajes de fallo precisos, mucho más útiles que un booleano. Fíjate en el segundo test de abajo: verifica el fallo y luego la recuperación justo después. Los caminos de error son código como cualquier otro: si no se testean, dalos por rotos.

DART
import 'package:flutter_test/flutter_test.dart';

class FakeScoreRepository implements ScoreRepository {
  int calls = 0;
  bool failNextCall = false;

  @override
  Future<int> fetchBest() async {
    calls++;
    if (failNextCall) {
      failNextCall = false;
      throw Exception('network unavailable');
    }
    return 42;
  }
}

void main() {
  late FakeScoreRepository repository;
  late ScoreController controller;

  setUp(() {
    repository = FakeScoreRepository();
    controller = ScoreController(repository);
  });

  test('exposes the best score', () async {
    await controller.load();

    expect(controller.best, 42);
    expect(controller.error, isNull);
    expect(repository.calls, 1);
  });

  test('captures an error then recovers', () async {
    repository.failNextCall = true;

    await controller.load();
    expect(controller.error, isA<Exception>());

    await controller.load();
    expect(controller.best, 42);
    expect(controller.error, isNull);
  });

  test('notifies its listeners', () async {
    var notifications = 0;
    controller.addListener(() => notifications++);

    await controller.load();

    expect(notifications, 1);
  });
}
Un fake de quince líneas reemplaza toda una librería de mocks.

Un test de widget no renderiza nada en la pantalla: se ejecuta en un binding especializado (AutomatedTestWidgetsFlutterBinding) donde el tiempo está simulado y donde cada frame se produce bajo demanda. pumpWidget monta el árbol; pump() avanza el reloj falso y produce un frame; pump(const Duration(milliseconds: 150)) da un salto en el tiempo, ideal para congelar un estado intermedio de animación e inspeccionarlo. Los Finder (find.text, find.byType, find.byKey) consultan el árbol real de Elements, no una captura de pantalla. Como nada es verdaderamente asíncrono, ni el reloj ni la red, estos tests son deterministas: un fallo siempre es reproducible.

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

class BadgeScreen extends StatefulWidget {
  const BadgeScreen({super.key});

  @override
  State<BadgeScreen> createState() => _BadgeScreenState();
}

class _BadgeScreenState extends State<BadgeScreen> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          if (_visible) const Text('New'),
          IconButton(
            icon: const Icon(Icons.close),
            onPressed: () =>
                setState(() => _visible = false),
          ),
        ],
      ),
    );
  }
}

void main() {
  testWidgets('hides the badge after the tap',
      (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: BadgeScreen()),
    );
    expect(find.text('New'), findsOneWidget);

    await tester.tap(find.byIcon(Icons.close));
    await tester.pump();

    expect(find.text('New'), findsNothing);
  });
}
tap no dispara ningún frame: el pump que le sigue es obligatorio.
BASH
flutter test
flutter test test/score_controller_test.dart
flutter test --coverage
flutter test --update-goldens
Apunta a un archivo, mide la cobertura, regenera los goldens.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Por qué falla pumpAndSettle frente a una animación iniciada con repeat()?
    • Bombea frames mientras quede alguno programado: una animación infinita nunca se estabiliza, de ahí el timeout
    • repeat() no es compatible con el binding de test
    • pumpAndSettle solo gestiona las animaciones implícitas
  2. ¿Cuál es la diferencia entre un fake y un mock?
    • Un fake es una implementación simplificada pero funcional; un mock sirve sobre todo para verificar interacciones
    • El mock se escribe a mano, el fake lo genera una herramienta
    • Ninguna: los dos términos son sinónimos
    • Un fake no puede implementar una interfaz de Dart
  3. En un test de widget, ¿qué hace exactamente await tester.pump()?
    • Avanza el reloj simulado y produce un único frame nuevo
    • Espera a que terminen todas las animaciones en curso
    • Reinicia el test desde el principio
    • Espera a que terminen las peticiones de red reales