package:test es el framework oficial para pruebas en Dart. No es solo un ejecutor de funciones; es un motor que proporciona la estructura necesaria para validar el comportamiento de tu código de forma determinista. Para usarlo, escribes tus tests en archivos dentro de una carpeta test/ y los ejecutas con el comando dart test. La lógica se basa en tres pilares: test() para definir casos individuales, group() para organizar suites de pruebas y expect() para las aserciones, utilizando matchers para comparar resultados.
Si quieres asegurar que tu lógica de negocio es correcta, este es el camino. Funciona así porque separa la definición del test (el escenario) de la validación (el matcher). Debes usarlo siempre que desarrolles lógica que no sea trivial, especialmente en servicios, utilidades o lógica de negocio compleja. Si lo implementas mal, especialmente con código asíncrono, corres el riesgo de obtener “falsos positivos”: tests que marcan todo como verde incluso cuando el código ha fallado, simplemente porque el test terminó antes de que la asincronía completara su ejecución.
import 'package:test/test.dart';
class Usuario {
final String id;
final String nombre;
Usuario(this.id, this.nombre);
}
class ServicioUsuario {
final List<Usuario> _usuarios = [];
void registrar(Usuario usuario) {
if (usuario.nombre.isEmpty) {
throw ArgumentError('El nombre no puede estar vacío');
}
_usuarios.add(usuario);
}
Future<Usuario?> buscarPorId(String id) async {
// Simulamos latencia de red
await Future.delayed(const Duration(milliseconds: 10));
try {
return _usuarios.firstWhere((u) => u.id == id);
} catch (_) {
return null;
}
}
Stream<int> obtenerContador() async* {
for (var i in [1, 2, 3]) {
await Future.delayed(const Duration(milliseconds: 5));
yield i;
}
}
}
void main() {
group('ServicioUsuario', () {
late ServicioUsuario servicio;
// setUp se ejecuta antes de cada test individual para garantizar aislamiento
setUp(() {
servicio = ServicioUsuario();
});
test('Debe registrar un usuario válido', () {
final user = Usuario('u1', 'Ana');
servicio.registrar(user);
expect(user, isA<Usuario>());
expect(user.nombre, equals('Ana'));
});
test('Debe lanzar ArgumentError si el nombre es vacío', () {
final usuarioInvalido = Usuario('u2', '');
// Para probar excepciones, pasamos la función como un closure
expect(
() => servicio.registrar(usuarioInvalido),
throwsA(isA<ArgumentError>()),
);
});
group('Operaciones asíncronas', () {
test('Debe encontrar un usuario mediante un Future', () async {
final user = Usuario('u1', 'Ana');
servicio.registrar(user);
// Usamos completion para evaluar el resultado de un Future sin
// tener que llamar a 'await' manualmente en el cuerpo del test.
expect(servicio.buscarPorId('u1'), completion(equals(user)));
});
test('Debe emitir una secuencia de números mediante un Stream', () {
final stream = servicio.obtenerContador();
// Verificamos que el stream emita los valores esperados en orden
expect(stream, emitsInOrder([1, 2, 3]));
});
});
});
}
Desglose de la implementación
En el ejemplo anterior, hemos organizado todo bajo un group('ServicioUsuario', ...) para que los resultados en la terminal aparezcan jerarquizados.
Para asegurar que cada test empiece desde un estado limpio, utilizamos late ServicioUsuario servicio y lo inicializamos en el setUp. Esto evita que un usuario registrado en un test afecte la lista de usuarios en el siguiente, un error común que causa que los tests fallen de forma intermitente (flakiness).
En el caso de la validación de errores, nota que pasamos una función anónima () => servicio.registrar(...) a expect. Esto es crucial; si llamaras al método directamente, la excepción se lanzaría antes de que expect pudiera capturarla para evaluarla. Usamos throwsA(isA<ArgumentError>()) para ser precisos: no solo queremos que falle, sino que falle con el tipo de error correcto.
Para la parte asíncrona, el uso de completion es una técnica elegante. En lugar de escribir final result = await servicio.buscarPorId('u1'); expect(result, equals(user));, delegamos la espera al matcher completion. Esto hace que el código sea más declarativo. Con los Streams, emitsInOrder es el matcher ideal cuando necesitas validar una secuencia temporal de eventos, permitiendo que el motor de tests gestione la suscripción y la finalización del flujo de datos.
El error frecuente
Un error clásico cuando se empieza con tests asíncronos es olvidar el await o no usar matchers asíncronos, lo que resulta en un test que termina prematuramente.
// ❌ ERROR: Este test puede pasar incluso si el Future falla
test('Test con error silencioso', () async {
// Olvidamos el await o usar completion.
// El test ejecuta la línea, lanza el Future al event loop,
// y el cuerpo del test termina con éxito antes de que el Future falle.
servicio.buscarPorId('id-inexistente');
});
// ✅ CORRECTO: El test esperará a que el Future se resuelva
test('Test corregido con await', () async {
final resultado = await servicio.buscarPorId('id-inexistente');
expect(resultado, isNull);
});
// ✅ CORRECTO: El test usa un matcher asíncrono
test('Test corregido con completion', () {
expect(servicio.buscarPorId('id-inexistente'), completion(isNull));
});
Si no esperas la asincronía, la excepción se convierte en una “unhandled exception” que aparece en la consola de errores pero no marca el test como fallido, lo cual es extremadamente peligroso en entornos de CI/CD.
N° 95