Pruebas unitarias de Dart
Las pruebas unitarias son el medio fundamental para garantizar la calidad del código.
Verifican que el comportamiento de cada unidad funcional mínima (normalmente una función o método) sea el esperado.
Este capítulo presenta el uso del paquete test de Dart, la escritura de casos de prueba con test(), las aserciones con expect y las pruebas agrupadas con group().
Instalación y configuración del paquete test
Dart ofrece oficialmente el paquete test para escribir y ejecutar pruebas.
Primero añade la dependencia en pubspec.yaml:
# 文件路径:pubspec.yaml dev_dependencies: test: ^1.24.0
Luego ejecuta el siguiente comando para instalarlo:
$ dart pub get
Los archivos de prueba normalmente se colocan en el directorio test/ del proyecto, y sus nombres terminan en _test.dart.
Una estructura de proyecto típica:
my_project/ ├── lib/ │ └── calculator.dart # 被测试的代码 ├── test/ │ └── calculator_test.dart # 测试文件 └── pubspec.yaml
Escribir casos de prueba con test()
La función test() es la unidad básica para escribir casos de prueba.
Recibe dos parámetros: la descripción de la prueba (una cadena) y el cuerpo de la función de prueba.
Ejemplo
Código a probar (lib/calculator.dart):
class Calculator {
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
int multiply(int a, int b) => a * b;
double divide(int a, int b) {
if (b == 0) {
throw ArgumentError('El divisor no puede ser 0');
}
return a / b;
}
bool isEven(int n) => n % 2 == 0;
List<int> filterPositive(List<int> numbers) {
return numbers.where((n) => n > 0).toList();
}
}
Ejemplo
Archivo de prueba (test/calculator_test.dart):
import 'package:test/test.dart';
import 'package:my_project/calculator.dart';
void main() {
// Crear una instancia de Calculator para la prueba
var calculator = Calculator();
// Función test(): el primer parámetro es la descripción de la prueba, el segundo es la función de prueba
test('add() suma dos números positivos', () {
var result = calculator.add(3, 5);
// Aserción expect(): verifica si el resultado real es igual al valor esperado
expect(result, equals(8));
});
test('add() suma con números negativos', () {
expect(calculator.add(-3, 5), equals(2));
expect(calculator.add(-3, -5), equals(-8));
});
test('subtract() operación de resta', () {
expect(calculator.subtract(10, 3), equals(7));
});
test('multiply() operación de multiplicación', () {
expect(calculator.multiply(4, 5), equals(20));
// Multiplicar por cero
expect(calculator.multiply(100, 0), equals(0));
});
test('divide() división normal', () {
expect(calculator.divide(10, 2), equals(5.0));
expect(calculator.divide(7, 2), equals(3.5));
});
test('isEven() verifica si es par', () {
expect(calculator.isEven(4), isTrue);
expect(calculator.isEven(5), isFalse);
});
}
Ejecutar las pruebas:
$ dart test
00:00 +6: All tests passed!
Aserciones con expect
expect() es la función más central en las pruebas; verifica si el valor real cumple una condición.
Sintaxis básica: expect(actual, matcher).
Matchers comunes
| Matcher | Uso | Ejemplo |
|---|---|---|
| equals(expected) | Verificar que los valores sean iguales | expect(result, equals(42)) |
| isTrue / isFalse | Verificar valores booleanos | expect(flag, isTrue) |
| isNull / isNotNull | Verificar null | expect(value, isNull) |
| contains(value) | Contiene un elemento (lista) o subcadena (cadena) | expect(list, contains('a')) |
| isA | Verificar el tipo | expect(obj, isA |
| throws | Verificar que se lance una excepción | expect(() => f(), throwsException) |
| isNotEmpty | No vacío | expect(list, isNotEmpty) |
| hasLength(n) | Verificar la longitud | expect(list, hasLength(3)) |
| greaterThan(n) | Mayor que | expect(score, greaterThan(60)) |
| closeTo(value, delta) | Números de punto flotante aproximadamente iguales | expect(3.14, closeTo(3.1, 0.1)) |
Ejemplo
Aplicación práctica de varios Matchers:
void main() {
test('EJEMPLO ejemplos de varias aserciones', () {
// Igualdad básica
expect(2 + 2, equals(4));
// Valores booleanos
expect('hello'.contains('h'), isTrue);
expect(''.isEmpty, isTrue);
// Comprobación de null
String? name;
expect(name, isNull);
name = 'EJEMPLO';
expect(name, isNotNull);
// Comprobación de tipo
expect('EJEMPLO', isA<String>());
expect(42, isA<int>());
// Contenidos en listas/cadenas
expect([1, 2, 3], contains(2));
expect('Hello, EJEMPLO!', contains('EJEMPLO'));
// Longitud
expect([1, 2, 3], hasLength(3));
expect('Dart', hasLength(4));
// Comparación numérica
expect(100, greaterThan(50));
expect(30, lessThan(60));
expect(75, greaterThanOrEqualTo(60));
// Comparación de punto flotante (evita problemas de precisión)
expect(0.1 + 0.2, closeTo(0.3, 0.001));
// Colección no vacía
expect([1, 2], isNotEmpty);
// Igualdad de listas
expect([1, 2, 3], equals([1, 2, 3]));
// Map contiene una clave
var user = {'name': 'ejemplo', 'age': 10};
expect(user, containsPair('name', 'ejemplo'));
expect(user.keys, contains('age'));
});
}
00:00 +1: All tests passed!
Pruebas de excepciones
Ejemplo
int divide(int a, int b) {
if (b == 0) throw ArgumentError('El divisor no puede ser 0');
return a ~/ b;
}
void main() {
test('divide() debería lanzar una excepción al dividir por cero', () {
// Verificar que se lance cualquier excepción
expect(() => divide(10, 0), throwsException);
// Verificar que se lance una excepción de un tipo específico
expect(() => divide(10, 0), throwsArgumentError);
// Verificar el mensaje de la excepción
expect(
() => divide(10, 0),
throwsA(predicate((e) =>
e is ArgumentError &&
e.message.contains('divisor'))),
);
});
test('divide() no lanza excepción en condiciones normales', () {
expect(() => divide(10, 2), returnsNormally);
expect(divide(10, 2), equals(5));
});
}
00:00 +2: All tests passed!
Al probar excepciones, es necesario envolver el código que puede lanzar una excepción en una función anónima (() => code), en lugar de llamarlo directamente. Si escribes directamente expect(divide(10, 0), throwsException), divide lanzará la excepción inmediatamente y expect no tendrá oportunidad de ejecutarse.
Pruebas agrupadas con group()
group() se utiliza para organizar casos de prueba relacionados y hacer que la estructura de pruebas sea más clara.
Ejemplo
// Función bajo prueba
class StringUtils {
static String capitalize(String s) {
if (s.isEmpty) return s;
return s[0].toUpperCase() + s.substring(1);
}
static String reverse(String s) {
return s.split('').reversed.join('');
}
static bool isPalindrome(String s) {
var clean = s.toLowerCase().replaceAll(' ', '');
return clean == reverse(clean);
}
static int countWords(String s) {
if (s.trim().isEmpty) return 0;
return s.trim().split(RegExp(r'\s+')).length;
}
}
void main() {
// Anidación de group() para organizar pruebas
group('StringUtils', () {
// Subgrupo
group('capitalize()', () {
test('Las palabras normales comienzan con mayúscula', () {
expect(StringUtils.capitalize('hello'), equals('Hello'));
});
test('Las palabras ya en mayúscula no cambian', () {
expect(StringUtils.capitalize('Hello'), equals('Hello'));
});
test('La cadena vacía devuelve una cadena vacía', () {
expect(StringUtils.capitalize(''), equals(''));
});
test('Un solo carácter', () {
expect(StringUtils.capitalize('a'), equals('A'));
});
});
group('reverse()', () {
test('Invertir una cadena normal', () {
expect(StringUtils.reverse('EJEMPLO'), equals('BOONUR'));
});
test('Invertir una cadena palíndroma no cambia', () {
expect(StringUtils.reverse('aba'), equals('aba'));
});
test('Cadena vacía', () {
expect(StringUtils.reverse(''), equals(''));
});
});
group('isPalindrome()', () {
test('Una cadena palíndroma devuelve true', () {
expect(StringUtils.isPalindrome('racecar'), isTrue);
expect(StringUtils.isPalindrome('A man a plan a canal Panama'), isTrue);
});
test('Una cadena no palíndroma devuelve false', () {
expect(StringUtils.isPalindrome('hello'), isFalse);
});
test('La cadena vacía se considera palíndroma', () {
expect(StringUtils.isPalindrome(''), isTrue);
});
});
group('countWords()', () {
test('Oración normal', () {
expect(StringUtils.countWords('Hello World Dart'), equals(3));
});
test('Espacios adicionales', () {
expect(StringUtils.countWords(' Hello World '), equals(2));
});
test('Cadena vacía', () {
expect(StringUtils.countWords(''), equals(0));
expect(StringUtils.countWords(' '), equals(0));
});
});
});
}
00:00 +12: All tests passed!
group() se puede anidar para crear una estructura jerárquica de pruebas.
Esto hace que el informe de pruebas sea más legible y facilita localizar rápidamente las pruebas fallidas.
Una buena estrategia de agrupación es organizar según el 'módulo/clase/método bajo prueba'. De este modo, cuando una prueba falle, podrás saber de inmediato qué funcionalidad tiene el problema.
setUp y tearDown
setUp se ejecuta antes de cada test, y tearDown se ejecuta después de cada test.
Se utilizan para preparar el entorno de prueba y limpiar recursos.
Ejemplo
// Simular una clase que necesita inicialización y limpieza
class Database {
bool isConnected = false;
void connect() {
isConnected = true;
print(' Base de datos conectada');
}
void disconnect() {
isConnected = false;
print(' Base de datos desconectada');
}
String query(String sql) {
if (!isConnected) throw Exception('Base de datos no conectada');
return 'Resultado de la consulta: $sql';
}
}
void main() {
group('Prueba de Database', () {
late Database db; // Inicialización diferida con late
// Se ejecuta antes de cada test
setUp(() {
db = Database();
db.connect();
print(' [setUp] Preparando el entorno de prueba');
});
// Se ejecuta después de cada test
tearDown(() {
db.disconnect();
print(' [tearDown] Limpiando el entorno de prueba');
});
test('query() consulta normal', () {
var result = db.query('SELECT * FROM users');
expect(result, contains('EJEMPLO') ? result.contains('users') : result.contains('users'));
// Aserción simplificada
expect(result, isNotEmpty);
});
test('query() puede ejecutar múltiples consultas después de conectarse', () {
var r1 = db.query('SELECT 1');
var r2 = db.query('SELECT 2');
expect(r1, isNotEmpty);
expect(r2, isNotEmpty);
});
test('La consulta sin conexión lanza una excepción', () {
db.disconnect(); // Desconexión manual
expect(() => db.query('SELECT 1'), throwsException);
});
});
}
数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 数据库已连接 [setUp] 准备测试环境 数据库已断开 [tearDown] 清理测试环境 00:00 +3: All tests passed!
Observa el orden de ejecución de setUp y tearDown en la salida: se ejecutan una vez antes y después de cada test.
setUp y tearDown garantizan que cada prueba comience desde un estado limpio, sin verse afectada por otras pruebas. Esta es la garantía clave de la independencia de las pruebas.
Pruebas asíncronas
El paquete test de Dart admite pruebas asíncronas de forma nativa.
Las funciones de prueba pueden devolver un Future; el framework esperará automáticamente a que el Future se complete.
Ejemplo
// Función asíncrona: simula una solicitud de red
Future<String> fetchUserData(int userId) async {
await Future.delayed(Duration(milliseconds: 100));
if (userId <= 0) {
throw Exception('ID de usuario no válido');
}
return 'Datos del usuario $userId';
}
Future<List<int>> fetchNumbers() async {
await Future.delayed(Duration(milliseconds: 50));
return [1, 2, 3, 4, 5];
}
void main() {
group('Prueba asíncrona', () {
test('fetchUserData() obtiene los datos correctamente', () async {
var data = await fetchUserData(1);
expect(data, equals('Datos del usuario 1'));
});
test('fetchUserData() lanza una excepción con ID no válido', () async {
expect(
() => fetchUserData(-1),
throwsA(isA<Exception>()),
);
});
test('fetchNumbers() devuelve la lista correcta', () async {
var numbers = await fetchNumbers();
expect(numbers, hasLength(5));
expect(numbers, contains(3));
expect(numbers.first, equals(1));
});
// Prueba de múltiples operaciones asíncronas
test('Operaciones asíncronas consecutivas', () async {
var data1 = await fetchUserData(1);
var data2 = await fetchUserData(2);
expect(data1, isNot(equals(data2)));
expect(data1, contains('1'));
expect(data2, contains('2'));
});
});
}
00:00 +4: All tests passed!
Comandos comunes para ejecutar pruebas
| Comando | Función |
|---|---|
| dart test | Ejecutar todas las pruebas |
| dart test test/calculator_test.dart | Ejecutar un archivo de prueba específico |
| dart test --name="add" | Ejecutar solo las pruebas cuyo nombre contenga "add" |
| dart test --concurrency=4 | Ejecutar pruebas de forma concurrente (aceleración) |
| dart test --reporter=expanded | Modo de salida detallada |
| dart test --coverage=coverage | Generar datos de cobertura de pruebas |
Buenas prácticas de pruebas
| Práctica | Explicación |
|---|---|
| Cada prueba verifica solo una cosa | Un test() corresponde a un comportamiento; falla más rápido la localización |
| El nombre de la prueba describe el comportamiento, no la implementación | "add() suma dos números positivos" es mejor que "Prueba de add()" |
| Patrón AAA | Arrange (Preparar) → Act (Ejecutar) → Assert (Afirmar) |
| Escribir primero la prueba que falla | Confirma que la prueba puede capturar el error, luego escribe código para que pase |
| Pruebas de condiciones límite | Valores nulos, cero, negativos, valores máximos, valores mínimos |
| Mantener las pruebas independientes entre sí | El resultado de una prueba no debe afectar a otra prueba |
Las pruebas no son una carga, sino un seguro. Cuantas más pruebas escribas, más confianza tendrás al refactorizar. Cuando una prueba falla, no necesitas adivinar dónde está el problema: la prueba te lo dirá con precisión.
Resumen del capítulo
Este capítulo presenta el flujo completo de las pruebas unitarias en Dart: instalación del paquete test, escritura de casos de prueba con test(), uso de aserciones con expect, organización en grupos con group(), gestión del entorno con setUp/tearDown y pruebas asíncronas.
Las pruebas unitarias son una asignatura obligatoria para los desarrolladores profesionales; adquirir el hábito de escribir pruebas hará que la calidad de tu código mejore continuamente.
Resumen del tutorial completo
¡Felicidades! Has completado el aprendizaje introductorio del lenguaje de programación Dart.
Repasemos la ruta de aprendizaje de estos 21 capítulos:
| Etapa | Capítulo | Contenido principal |
|---|---|---|
| I. Fundamentos de introducción | Capítulos 1-4 | Visión general de Dart, configuración del entorno, primer programa, sintaxis básica |
| II. Tipos principales y flujo de control | Capítulos 5-8 | Variables y tipos de datos, operadores, flujo de control, colecciones |
| III. Funciones y programación orientada a objetos | Capítulos 9-13 | Funciones, clases y objetos, herencia y polimorfismo, interfaces y Mixins, genéricos |
| IV. Características avanzadas | Capítulos 14-17 | Enumeraciones y símbolos, manejo de excepciones, gestión de paquetes y bibliotecas, typedef |
| V. Programación asíncrona y pruebas | Capítulos 18-21 | Programación asíncrona, flujos Stream, concurrencia e Isolates, pruebas unitarias |
Al dominar estos contenidos, ya tienes la capacidad de utilizar Dart para el desarrollo diario.
El siguiente paso es profundizar enel framework Flutter, aplicando los conocimientos de Dart al desarrollo de UI en móvil y web.
Otras extensiones