Test Unitarios con Deno

¿Qué es BDD?

BDD (Behavior-Driven Development) es una metodología de desarrollo que coloca el comportamiento del software en el centro del proceso. A diferencia del testing unitario tradicional que se enfoca en “¿qué hace esta función?”, BDD se pregunta “¿cuál es el comportamiento que espera el usuario o el negocio?”.

En BDD, los tests se escriben usando una estructura clara y legible:

  • Given (Dado): El contexto o estado inicial.
  • When (Cuando): La acción que realizamos.
  • Then (Entonces): El resultado esperado.

Esta estructura hace que los tests sean casi documentación viva del sistema. No solo validan que el código funciona, sino que también comunican por qué y cómo debe comportarse.

Fortalezas de BDD

  1. Claridad: Los tests se leen como especificaciones. Cualquiera (técnico o no) puede entender qué se está probando.
  2. Documentación actualizada: El comportamiento del código siempre está documentado en los tests; no se queda obsoleto como un README abandonado.
  3. Colaboración efectiva: Diseñadores, QA y desarrolladores pueden discutir el comportamiento esperado usando el mismo lenguaje.
  4. Menos bugs por ambigüedad: Al escribir primero el comportamiento esperado, se reducen las interpretaciones erróneas.
  5. Refactoring seguro: Con tests BDD claros, puedes modificar la implementación sin miedo a romper el comportamiento.

Por qué BDD en Deno

Deno nació con premisas fuertes en seguridad, modernidad y simplicidad. No sorprende que su sistema de testing incorpore características que se alinean perfectamente con BDD:

  • Sistema de testing built-in sin dependencias externas (no necesitas instalar nada adicional).
  • Sintaxis moderna con describe() e it() para expresar comportamiento.
  • Seguridad de permisos por defecto (los tests se ejecutan en un sandbox; solo permites lo que necesitas).
  • TypeScript out-of-the-box sin configuración.

Esto convierte a Deno en una plataforma ideal para practicar BDD sin fricción.

Iniciemos un proyecto de prueba

Crearemos esta estructura de directorios para ir creando los archivos necesarios para una prueba simple.

Estructura de directorios
calc-project/
├── src/
│ └── Calc.ts
├── test/
│ └── Calc.test.ts
└── deno.json

El contenido del archivo deno.json es el siguiente. Recuerda que en este archivo es donde se declaran las dependencias y las tareas ejecutables.

1
2
3
4
5
6
7
{
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.15",
"@std/expect": "jsr:@std/expect@^1.0.17",
"@std/testing": "jsr:@std/testing@^1.0.16"
}
}

Ahora revisemos el contenido de los archivos del proyecto. El primero es una clase con 3 operaciones matemáticas simples: suma, resta y división.

Calc.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* Simple calculator utilities used in examples and tests.
*
* Esta clase es intencionalmente pequeña y tiene métodos básicos
* para demostrar pruebas unitarias en estilo BDD con Deno.
*/
export class Calc {
/**
* Suma dos números.
* @param a - primer sumando
* @param b - segundo sumando
* @returns la suma de `a` y `b`
*/
public sum(a: number, b: number): number {
return a + b;
}

/**
* Resta dos números (a - b).
* @param a - minuendo
* @param b - sustraendo
* @returns la resta de `a` menos `b`
*/
public subtract(a: number, b: number): number {
return a - b;
}

/**
* Divide dos números (a / b).
*
* Lanza un error cuando `b` es 0 o no es un número válido (NaN),
* para evitar divisiones inválidas.
*
* @param a - dividendo
* @param b - divisor (no debe ser 0 ni NaN)
* @returns el resultado de `a / b`
* @throws {Error} si `b` es 0 o NaN
*/
public div(a: number, b: number): number {
if (b === 0 || Number.isNaN(b)) {
throw new Error("No se puede dividir por 0");
}
return a / b;
}
}

A continuación, veremos cómo se implementan los tests unitarios sobre la clase Calc usando el estilo BDD que nos ofrece Deno y las dependencias de @std/testing y @std/expect.

Test unitarios sobre la clase Calc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { expect } from "@std/expect/expect";
import { describe, it } from "@std/testing/bdd";
import { Calc } from "../src/Calc.ts";

describe("Test unitarios sobre la clase Calc", () => {
describe("sum - casos adicionales (BDD)", () => {
it("suma números positivos", () => {
const calc = new Calc();
expect(calc.sum(2, 3)).toEqual(5);
expect(calc.sum(10, 20)).toEqual(30);
});
});

describe("subtract - casos adicionales (BDD)", () => {
it("resta números positivos", () => {
const calc = new Calc();
expect(calc.subtract(5, 3)).toEqual(2);
expect(calc.subtract(20, 10)).toEqual(10);
});
});

describe("div - validaciones", () => {
it("debería fallar dividiendo por 0", () => {
const calc = new Calc();
// Capturar el error de ejecución
expect(() => calc.div(1, 0)).toThrow("No se puede dividir por 0");
});

it("debería dividir correctamente 8/2=4", () => {
const calc = new Calc();
const result = calc.div(8, 2);
expect(result).toEqual(4);
});
});
});

Para ejecutar todos los tests, usamos el comando deno test. Este buscará y correrá todos los archivos de test en el proyecto, generando una salida similar a la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
running 4 tests from ./test/Calc.test.ts
Test unitarios sobre la clase Calc ...
sum - casos adicionales (BDD) ...
suma números positivos ... ok (1ms)
sum - casos adicionales (BDD) ... ok (1ms)
subtract - casos adicionales (BDD) ...
resta números positivos ... ok (0ms)
subtract - casos adicionales (BDD) ... ok (0ms)
div - validaciones ...
debería fallar dividiendo por 0 ... ok (1ms)
debería dividir correctamente 8/2=4 ... ok (0ms)
div - validaciones ... ok (1ms)
Test unitarios sobre la clase Calc ... ok (2ms)

ok | 4 passed | 0 failed (10ms)

En este bloque de código, estamos definiendo la estructura de nuestros tests utilizando el estilo BDD que nos proporciona Deno:

  1. describe("Test unitarios sobre la clase Calc", ...): Este es el bloque principal que agrupa todos los tests relacionados con nuestra clase Calc. Funciona como un contenedor o una suite de pruebas.

  2. describe("sum - casos adicionales (BDD)", ...): Dentro de la suite principal, creamos un sub-grupo específico para las pruebas del método sum. Esto ayuda a organizar y contextualizar los tests.

  3. it("suma números positivos", ...): Aquí definimos un caso de prueba concreto. La descripción "suma números positivos" deja claro cuál es el comportamiento que estamos validando.

    • Dentro del it, creamos una instancia de Calc.
    • Usamos expect(calc.sum(2, 3)).toEqual(5); para afirmar que el resultado de llamar a sum(2, 3) es igual a 5. La función expect viene de la librería @std/expect y nos ofrece una forma legible y expresiva de hacer aserciones.
  4. describe("subtract - casos adicionales (BDD)", ...): De manera similar, creamos otro sub-grupo para el método subtract, manteniendo nuestros tests bien organizados.

  5. describe("div - validaciones", ...): Finalmente, un grupo para el método div. Aquí se incluyen dos casos de prueba interesantes:

    • it("debería fallar dividiendo por 0", ...): Este test verifica que el código maneja los errores correctamente. Usamos expect(() => calc.div(1, 0)).toThrow(...) para asegurar que, al intentar dividir por cero, se lance un error con el mensaje esperado. Nota que la llamada a la función que debe fallar se envuelve en una función flecha () => ....
    • it("debería dividir correctamente 8/2=4", ...): Un test para el “camino feliz” o el caso de uso normal, asegurando que la división funciona como se espera.

Esta estructura jerárquica con describe e it no solo ejecuta el código, sino que también lo documenta de una manera que es fácil de leer y entender para cualquier persona en el equipo.

Ahora, profundicemos en cómo se estructuran y ejecutan los tests. La función describe es fundamental, ya que actúa como un agrupador que define un ámbito o scope para un conjunto de pruebas relacionadas. Dentro de este ámbito, podemos usar “hooks” (funciones especiales) para controlar el ciclo de vida de nuestros tests.

Estos son los hooks principales:

  • beforeAll: Se ejecuta una sola vez antes de que comiencen todos los tests dentro de su describe. Es ideal para preparar un estado inicial que no cambiará, como levantar un servidor o conectar a una base de datos de prueba.
  • beforeEach: Se ejecuta antes de cada test (it) dentro de su describe. Perfecto para resetear el estado entre pruebas, como limpiar una tabla de la base de datos o crear una nueva instancia de una clase.
  • afterEach: Se ejecuta después de cada test (it). Se usa para tareas de limpieza que deben ocurrir después de cada prueba, como borrar archivos temporales o cerrar una conexión.
  • afterAll: Se ejecuta una sola vez después de que todos los tests dentro de su describe hayan finalizado. Ideal para la limpieza final, como detener el servidor o cerrar la conexión a la base de datos.

La magia ocurre cuando anidamos bloques describe, ya que los hooks del ámbito exterior también se aplican a los ámbitos interiores.

Para ilustrar esto, veamos el siguiente código de ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { afterAll, afterEach, beforeAll, beforeEach, describe, it } from "@std/testing/bdd";

describe("Test - scope y hooks", () => {
beforeAll(() => {
console.log("Before All 1 (Scope Padre)");
});

afterAll(() => {
console.log("After All 1 (Scope Padre)");
});

beforeEach(() => {
console.log("Before Each 1 (Scope Padre)");
});

afterEach(() => {
console.log("After Each 1 (Scope Padre)");
});

it("should test 1", () => {
console.log("Test 1");
});

describe("Test - Scope Anidado", () => {
beforeAll(() => {
console.log("------> Before All 2 (Scope Anidado)");
});

afterAll(() => {
console.log("------> After All 2 (Scope Anidado)");
});

beforeEach(() => {
console.log("------> Before Each 2 (Scope Anidado)");
});

afterEach(() => {
console.log("------> After Each 2 (Scope Anidado)");
});

it("should test 2", () => {
console.log("------> Test 2");
});
});
});

La salida de esta ejecución revela el orden exacto en que Deno ejecuta cada bloque. Desglosemos lo que sucedió:

Salida: Orden de Ejecución
Test - scope y hooks ...
------- output -------
Before All 1 (Scope Padre)
----- output end -----
should test 1 ...
------- output -------
Before Each 1 (Scope Padre)
Test 1
After Each 1 (Scope Padre)
----- output end -----
should test 1 ... ok (0ms)
Test - Scope Anidado ...
------- output -------
------> Before All 2 (Scope Anidado)
----- output end -----
should test 2 ...
------- output -------
Before Each 1 (Scope Padre)
------> Before Each 2 (Scope Anidado)
------> Test 2
------> After Each 2 (Scope Anidado)
After Each 1 (Scope Padre)
----- output end -----
should test 2 ... ok (0ms)
------- output -------
------> After All 2 (Scope Anidado)
----- output end -----
Test - Scope Anidado ... ok (0ms)
------- output -------
After All 1 (Scope Padre)
----- output end -----
Test - scope y hooks ... ok (1ms)

ok | 1 passed (3 steps) | 0 failed (3ms)
  1. Inicio del Scope Padre: Se ejecuta beforeAll del scope padre (Before All 1).
  2. Ejecución del primer test:
    • Se ejecuta beforeEach del padre (Before Each 1).
    • Se ejecuta el cuerpo del Test 1.
    • Se ejecuta afterEach del padre (After Each 1).
  3. Inicio del Scope Anidado:
    • Se ejecuta el beforeAll del scope anidado (Before All 2).
  4. Ejecución del segundo test (anidado):
    • Se ejecuta beforeEach del padre (Before Each 1), porque el test anidado está dentro de su ámbito.
    • Se ejecuta beforeEach del scope anidado (Before Each 2).
    • Se ejecuta el cuerpo del Test 2.
    • Se ejecuta afterEach del scope anidado (After Each 2).
    • Se ejecuta afterEach del padre (After Each 1).
  5. Fin del Scope Anidado: Se ejecuta afterAll del scope anidado (After All 2).
  6. Fin del Scope Padre: Se ejecuta afterAll del scope padre (After All 1).

Como puedes ver, la función describe no solo agrupa tests, sino que crea una jerarquía. Los hooks del describe padre “envuelven” a los hooks y tests de los describe hijos, permitiendo crear configuraciones y limpiezas complejas y ordenadas. Esto es clave para escribir tests mantenibles y escalables.

Palabras al Cierre

Espero que esta guía te haya dado una buena base para empezar a escribir tests unitarios robustos y expresivos en Deno usando BDD. Esta metodología no solo mejora la calidad de tu código, sino que también ayuda a que otros desarrolladores entiendan rápidamente el objetivo de cada test y lo que se busca lograr.

Si hay algún otro tema sobre Deno o testing que te gustaría que explorara, ¡déjamelo saber en los comentarios!


Referencias

Aquí tienes algunos enlaces para profundizar en los conceptos que hemos cubierto:

Author

Francisco Collao

Posted on

2025-10-31

Updated on

2025-11-01

Licensed under

Comentarios