Una simple herramienta de testeo en Javascript, para propósitos educativos. Disponible en npm: @pmoo/testy.
➡️ English version here 👷 Guías para contribuir
npm install --save-dev @pmoo/testy
(si utilizas npm)
yarn add --dev @pmoo/testy
(si utilizas yarn)
Versiones de Node soportadas: 18.x o mayor (todas las versiones con soporte activo o de seguridad listadas aquí)
Una suite de test no es más que un archivo cuyo nombre finaliza con _test.js
y tiene la siguiente forma:
import { suite, test, assert } from '@pmoo/testy';
suite('una suite de tests aburrida', () => {
test('42 es 42, no nos sorprende', () => {
assert.that(42).isEqualTo(42);
});
});
Una suite representa un agrupamiento de tests, y se define llamando a la función suite(name, body)
, que toma como parámetro el nombre de este agrupamiendo y una función sin argumentos, que representa el contenido de la suite.
Un test se escribe llamando a la función test(name, body)
, que toma como parámetro el nombre del caso de test y una función sin parámetros que representa el cuerpo del test.
Dentro del test se pueden evaluar diferentes aserciones que están documentadas más adelante.
Puedes ejecutar una suite de test con el siguiente comando:
$ npx testy my_test.js
Or, al ejecutar testy
sin argumentos se ejecutarán todos los tests, por defecto, que están dentro del directorio tests
:
$ npx testy
También se puede registrar testy
como script de test
script en package.json
:
{
...
"scripts": {
"test": "npx testy"
},
...
}
Para luego ejecutar los tests con npm test
o yarn test
.
Testy se puede configurar a través de un archivo llamado .testyrc.json
que debe ser declarado en el directorio raíz del proyecto. Puedes usar la siguiente configuración como plantilla (los valores aquí mencionados son los valores por defecto):
{
"directory": "./tests", // directorio con los archivos de test
"filter": ".*_test.js$", // qué convención utilizar para el nombrado de archivos de test
"language": "en", // idioma de los mensajes de salida ("en" y "es" soportados por el momento)
"failFast": false, // habilita/deshabilita el modo "fail fast" (detener la ejecución en el primer fallo)
"randomOrder": false // habilita/deshabilita la ejecución de tests en orden aleatorio.
"timeoutMs": 1000 // asigna el tiempo límite de ejecución por cada test (en milisegundos)
}
Estos son todos los parámetros de configuración que existen, ajústalos de acuerdo a tus necesidades.
Siguiendo este ejemplo de configuración, lo que se va a ejecutar es cada suite de test dentro del directorio tests
, cuyos nombres de archivos finalicen con *test.js
.
- Aserciones sobre valores booleanos:
assert.that(boolean).isTrue()
oassert.isTrue(boolean)
. Realiza una comparación estricta contratrue
(object === true
)assert.that(boolean).isFalse()
oassert.isFalse(boolean)
. Realiza una comparación estricta contrafalse
(object === false
)
- Aserciones de igualdad de objetos:
assert.that(actual).isEqualTo(expected)
oassert.areEqual(actual, expected)
.assert.that(actual).isNotEqualTo(expected)
oassert.areNotEqual(actual, expected)
- Las aserciones de igualdad utilizan una comparación (deep) basada en el módulo
assert
de Node, y falla si los objetos que están siendo comparados tienen referencias cíclicas. - El criterio de igualdad en objetos no primitivos puede ser especificado:
- Pasando una función adicional de comparación de dos parámetros a
isEqualTo(expected, criteria)
oareEqual(actual, expected, criteria)
- Pasando un nombre de método que el objeto
actual
comprenda:isEqualTo(expected, 'myEqMessage')
oareEqual(actual, expected, 'myEqMessage')
- Por defecto, si
actual
entiende el mensajeequals
, será utilizado para determinar la comparación - Si comparamos
undefined
conundefined
usandoisEqualTo()
, el test fallará. Para chequear explícitamente por el valorundefined
, se debe utilizar las asercionesisUndefined()
oisNotUndefined()
documentadas más adelante.
- Pasando una función adicional de comparación de dos parámetros a
- Aserciones de identidad de objetos:
assert.that(actual).isIdenticalTo(expected)
oassert.areIdentical(actual, expected)
assert.that(actual).isNotIdenticalTo(expected)
oassert.areNotIdentical(actual, expected)
- Las aserciones de identidad comprueban si dos referencias apuntan al mismo objeto utilizando el operador
===
.
- Validar si un objeto es o no
undefined
:assert.that(aValue).isUndefined()
oassert.isUndefined(aValue)
assert.that(aValue).isNotUndefined()
oassert.isNotUndefined(aValue)
- Validar si un objeto es o no
null
:assert.that(aValue).isNull()
oassert.isNull(aValue)
assert.that(aValue).isNotNull()
oassert.isNotNull(aValue)
- Testeo de errores:
assert.that(() => { ... }).raises(error)
o con una expresión regular.raises(/part of message/)
assert.that(() => { ... }).doesNotRaise(error)
assert.that(() => { ... }).doesNotRaiseAnyErrors()
- Aserciones numéricas:
- Comparación:
assert.that(aNumber).isGreaterThan(anotherNumber)
assert.that(aNumber).isLessThan(anotherNumber)
assert.that(aNumber).isGreaterThanOrEqualTo(anotherNumber)
assert.that(aNumber).isLessThanOrEqualTo(anotherNumber)
- Redondeo
assert.that(aNumber).isNearTo(anotherNumber)
. Se puede pasar un segundo parámetro adicional que indica el número de dígitos de precisión que se van a considerar. Por defecto, son4
.
- Comparación:
- Aserciones sobre strings:
assert.that(string).matches(regexOrString)
oassert.isMatching(string, regexOrString)
- Inclusión de objetos en colecciones (
Array
ySet
):assert.that(collection).includes(object)
assert.that(collection).doesNotInclude(object)
assert.that(collection).includesExactly(...objects)
- Verificar si una colección es o no vacía:
assert.that(collection).isEmpty()
orassert.isEmpty(collection)
assert.that(collection).isNotEmpty()
orassert.isNotEmpty(collection)
- la colección a verificar puede ser un
Array
, unString
o unSet
En la carpeta tests
podrás encontrar más ejemplos y todas las posibles aserciones que puedes escribir. Testy está testeado en sí mismo.
-
Ejecutar código antes y después de cada test: al igual que muchas herramientas de testing, existe una forma de ejecutar código antes y después de cada test haciendo uso de
before()
yafter()
como parte de la definición de una suite.before()
yafter()
reciben una función como parámetro y pueden utilizarse una sola vez por suite. Ejemplo:import { suite, test, assert, before, after } from '@pmoo/testy'; suite('usando las funciones before() y after()', () => { let answer; before(() => { answer = 42; }); test('la respuesta es 42', () => { assert.that(answer).isEqualTo(42); }); after(() => { answer = undefined; }); });
-
Soporte para tests pendientes: un test que no tenga cuerpo, será reportado como pendiente (
[PENDIENTE]
) y no se considerará una falla. -
Soporte para tests excluidos: un test se puede excluir añadiendo
.skip()
al final de su definición, esto lo reportará como[NO EJECUTADO]
.```javascript import { suite, test, assert } from '@pmoo/testy'; suite('Ejecutando una suite con test excluido', () => { test('Estoy excluido', async () => { assert.that(1).isEqualTo(1); }).skip(); }); ```
-
Soporte para tests asíncronos: si el código que estás testeando requiere de
async
, es posible hacerawait
dentro de la definicion del test y luego escribir las aserciones. También es posible hacer llamados asincrónicos enbefore()
yafter()
. Ejemplo:import { suite, test, assert, before } from '@pmoo/testy'; const promesaUno = async () => Promise.resolve(42); const promesaDos = async () => Promise.resolve(21); suite('usando async y await', () => { let respuestaUno; before(async () => { respuestaUno = await promesaUno(); }); test('comparando resultados de promesas', async () => { const respuestaDos = await promesaDos(); assert.that(respuestaUno).isEqualTo(42); assert.that(respuestaDos).isEqualTo(21); }); });
-
Modo "fail-fast": cuando está habilitado, se detiene apenas encuentra un test que falle o lance un error. Los tests restantes serán marcados como no ejecutados (skipped).
-
Ejecutar tests en orden aleatorio: una buena suite de tests no depende de un orden particular de tests para ejecutarse correctamentee. Activando esta configuración es una buena forma de asegurar eso.
-
Chequeo estricto de presencia de aserciones: si un test no evalúa ninguna aserción durante su ejecución, el resultado se considera un error. Básicamente, un test que no tiene aserciones es un "mal" test.
-
Explícitamente, marcar un test como fallido o pendiente: Ejemplos:
import { suite, test, fail, pending } from '@pmoo/testy'; suite('marcando tests explícitamente como fallidos o pendientes', () => { test('marcando como fallido', () => fail.with('no debería estar aquí')); test('marcando como pendiente', () => pending.dueTo('no hubo tiempo de finalizarlo')); });
Al ejecutar veremos los siguientes mensajes:
[FALLIDO] marcando como fallido => no debería estar aquí [PENDIENTE] marcando como pendiente => no hubo tiempo de finalizarlo
¿Por qué tener una herramienta de tests cuando ya existen otras? La razón principal es que deseamos mantener la simplicidad, algo que no se puede encontrar en las principales herramientas de testing conocidas.
- Cero dependencias: Este proyecto no depende de ningún otro paquete de npm para funcionar, lo que facilita su instalación, y lo hace más rápido: esencial para obtener feedback inmediato desarrollando con TDD. Esto es algo bueno también para instalar en lugares donde la conexión a internet no es buena y no queremos perder tiempo descargando múltiples dependencias.
- Código orientado a objetos entendible: Esta herramienta es utilizada para enseñar, así que es muy común durante las clases mirar el código para entender cómo se ejecutan los tests, para entender lo que sucede. El objetivo es que los alumnos puedan comprender la herramienta e incluso realizar contribuciones a ella. Intentamos seguir buenas prácticas de diseño con objetos y de clean code en general.
- Conjunto único de funcionalidad: Esta herramienta no sigue ninguna especificación ni trata de copiar la funcionalidad de enfoques conocidos de testing (como la forma "xUnit" la forma "xSpec"). La funcionalidad que existe, es la que tiene sentido que esté.
"Design Principles Behind Smalltalk" es una gran fuente de inspiración para este trabajo. Intentamos seguir los mismos principios aquí.
Por favor revisar la guía para contribuciones.
Muchas gracias a estas maravillosas personas (emoji key):
Este proyecto sigue la convención de all-contributors. Se aceptan contribuciones de todo tipo!