# Jest
**Jest es un framework de testing basado en RSpec** generalista que podemos utilizar en cualquier situación. Se trata de un framework flexible, rápido, con un output sencillo y comprensible, que nos permite completar un ciclo de feedback y con la máxima información en cada momento.
## Caracteristícas
- Fácil instalación y configuración
- Retroalimentación inmediata con modo watch
- Plataforma de testing con configuración simple
- Ejecución rápida y con entorno aislado
- Herramienta de code coverage integrada
- Potente librería de mocking
- Introduce el concepto de Snapshot testing
- Compatible con TypeScript: ts-jest
## Instalación y configuración
Lo primero que vamos a hacer es instalar Jest y sus dependencias con el gestor de paquetes de node npm. En este caso, como queremos que sea **compatible con TypeScript**, vamos a hacer uso de **ts-jest**. Ts-jest es un preprocesador con soporte para source maps que nos permite testear proyectos escritos en TypeScript (No hace falta importar las funciones de Jest).
Además de Jest y ts-jest vamos a instalar las **definiciones de tipos del propio Jest**.
```shell=
$npm i -D jest ts-jest @types/jest
```
Tras instalar las dependencias debemos ejecutar el comando de configuración de ts-jest.
```shell=
$npx ts-jest config:init
```
Este comando generará el fichero de configuración de Jest.
```javascript!
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
```
Abrimos el fichero y podemos ver que tenemos un objeto con dos valores: el **preset** y el **testEnvironment**. En este caso, el preset, es decir, el módulo que tiene la configuración preestablecida que ejecutará los test, es ts-jest. El test environment, en este caso, es node. También podría ser jsdom, por ejemplo.
Si queremos ejecutar los test desde npm, debemos añadir a la sección de scripts del fichero package.json:
```jsonld!
"scripts": {
"test": "jest",
}
```
Por último debemos decirle a TypeScript que no compile nuestros test, para que solo se ejecuten en TypeScript, para ello añadimos lo siguiente en el tsconfig:
```jsonld!
"exclude": [
"node_modules",
"src/tests/**/*"
]
```
Los test pueden estar en un directorio dentro de la carpeta del proyecto aunque, por convención, te los puedes encontrar en un directorio fuera del proyecto llamado __tests__ .
## Flags
Podemos pasarle flags a npm si antes de escribir el flag escribimos '--'.
- \<file>: En el caso de que solo queramos ejecutar una prueba basada en el nombre de un fichero o en el nombre de un directorio, podemos escribir tanto la ruta completa como el patrón que forma el nombre.
```shell!
$npm test -- file
```
- o: Se ejecutan las pruebas relacionadas con los archivos modificados basados en Git, es decir, se ejecutarán las pruebas relacionadas con el código que hayamos modificado.
```shell!
$npm test -- -o
```
- runInBand: Se ejecutan los test de forma síncrona
```shell!
$npm test -- --runInBand
```
- coverage: Nos permite ver el porcentaje de pruebas cubiertas.
```shell!
$npm test -- --coverage
```
- verbose: Si tenemos varios suites (Ficheros) de test, podemos agregar este flag para que nos aparezca la descripción de cada suite.
```shell!
$npm test -- --verbose
```
Si queremos que este comportamiento este por defecto podemos añadir en la configuración de jest:
```javascript!
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
verbose: true
};
```
## Ignorar y aislar test
Podemos ignorar test de dos formas:
- Añadiendo una x delante de las funciones test, it o describe
```javascript=
xdescribe("Skip all test", () => {
it("", () => {
...
})
})
describe("Skip some test", () => {
it("", () => {
...
})
xit("Skip", () => {
...
})
})
```
- Utilizando la propiedad ***skip*** de las funciones test, it o describe
```javascript=
describe.skip("Skip all test", () => {
it("", () => {
...
})
})
describe("Skip some test", () => {
it("", () => {
...
})
it.skip("Skip", () => {
...
})
})
```
Podemos aislar los test usando la propiedad ***only*** de las funciones test, it o describe, es importante saber que esto funciona a nivel de fichero.
```javascript=
describe.only("Skip all test", () => {
it("", () => {
...
})
})
describe("Skip some test", () => {
it("", () => {
...
})
it("Skip", () => {
...
})
})
```
## Modo Watch
Podemos ejecutar jest en modo watch con el siguiente comando:
```shell=
$jest --watch
```
Podríamos generar un script dentro del package.json de la siguiente forma:
```jsonld!
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
},
```
## Cobertura
### Informas de Cobertura
Podemos generar informes de cobertura añadiendo las siguientes opciones en el fichero de configuración de Jest:
```javascript=
module.exports = {
...
collectCoverage: true,
coverageDirectory: "./coverage"
};
```
De está forma se nos guardará en el directorio coverage el informe de cobertura, que podemos ver si abrimos el index.html que se encuentra en coverage/lcov-report.
El directorio no tiene porque llamarse coverage, puede llamarse como queramos.
Dicho informe se verá de la siguiente forma:

Si pulsamos en el link de cada test nos aparecerá el código del fichero testeado.

### Umbral de Cobertura
Jest nos permite establecer un mínimo de cobertura que deben tener nuestros test. Para ello modificamos el fichero de configuración:
```javascript=
module.exports = {
...
coverageThreshold: {
global: {
statements: 100,
branches: 100,
functions: 100,
lines: 100
}
}
};
```
En este caso estamos estableciendo una cobertura global del 100% en todos los casos.
También podemos especificar el mínimo de cobertura por ficheros, de tal forma que podemos tener una cobertura global y especifica para determinados suites. Esto lo hacemos indicando la ruta:
```javascript=
module.exports = {
...
coverageThreshold: {
global: {
statements: 100,
branches: 100,
functions: 100,
lines: 100
},
"src/test/file[js | ts]"
}
};
```
## Gestión del estado: before y after
- beforeEach: Ejecutamos una acción antes de cada test
- afterEach: Ejecutamos una acción después de cada test
- beforeAll: Ejecutamos una acción antes de todos los tests
- afterAll: Ejecutamos una acción despues de todos los test
```javascript=
describe("State management", () => {
beforeEach(() => {
console.log("Antes de cada test")
})
beforeAll(() => {
console.log("Antes de todos los tests")
})
afterEach(() => {
console.log("Después de cada test")
})
afterAll(() => {
console.log("Después de todos los tests")
})
it("Show someting", ()=> {
...
})
})
```
## Aserciones Explicitas
### Básicas
- **toBe**: Evalúa dos valores aplicando el método **Object.is()** (No nos vale para objetos y arrays). En el caso de querer comprobar que son distintos, podemos utilizar el matcher toBe precedido de not.
- **not**: Niega la aserción que le precede.ç
```javascript=
expect(1 + 2).toBe(3);
expect(1 + 2).not.toBe(4);
```
### Booleanas
- **toBeTruthy**: Verifica si un valor es verdadero
- **toBeFalsy**: Verifica si un valor es falso
- **toBeNull**: Verifica si un valor es nulo
- **toBeUndefined**: Verifica si un valor no está definido
- **toBeDefined**: Verifica si un valor está definido
```javascript=
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect({}).toBeDefined();
```
En JavaScript por defecto, todos los valores son verdaderos, salvo los conocidos como falsy, que son:
False, undefined, NaN, 0, -0, cero en formato big int y una cadena vacía.
```javascript=
expect(false).toBeFalsy();
expect(undefined).toBeFalsy();
expect(null).toBeFalsy();
expect(NaN).toBeFalsy();
expect(0).toBeFalsy();
expect(-0).toBeFalsy();
expect(0n).toBeFalsy();
expect('').toBeFalsy();
```
### Númericas
- **toBeGreatherThan**: Verifica que un número sea mayor que otro.
- **toBeGraterThanOrEqual**: Verifica que un número sea mayor o igual que otro.
- **toBeLessThan**: Verifica que un número sea menor que otro.
- **toBeLessThanOrEqual**: Verifica que un número sea mayor o igual que otro.
```javascript=
expect(1).toBeGreaterThan(0);
expect(1).toBeGreaterThanOrEqual(0);
expect(0).toBeLessThan(1);
expect(0).toBeLessThanOrEqual(1);
```
### String
- **toMatch**: Verifica el resultado de una expresión regular
```javascript=
expect('Aitor').toMatch(/tor/);
```
### Arrays y Objetos
- **toEqual**: Si queremos comparar que dos objetos o arrays sean iguales, toBe no nos sirve. Debemos hacer uso de toEqual. ToEqual, en esencia, **evalúa recursivamente cada uno de los campos de un objeto o cada uno de los elementos de un array**. También nos serviría para evaluar valores simples, al igual que toBe, y, por supuesto, podemos poner delante la aserción not.
```javascript=
// Objetos
//expect({ foo: 'foo', bar: 'bar' }).toBe({ foo: 'foo', bar: 'bar' }); //false
expect({ foo: 'foo', bar: 'bar' }).toEqual({ foo: 'foo', bar: 'bar' });
expect({ foo: 'foo', bar: 'bar' }).not.toEqual({ foo: 'foo', bar: 'foo' });
// Arrays
//expect([1, 2, 3]).toBe([1, 2, 3]); //false
expect([1, 2, 3]).toEqual([1, 2, 3]);
expect([1, 2, 3]).not.toEqual([0, 1, 2]);
```
- **toContain**: Verifica si un valor está contenido dentro de un array
- **toContainEqual**: Verifica que un objeto está contenido dentro de un array
```javascript=
expect([0, 1, 2]).toContain(2);
expect([
{ foo: 'foo', bar: 'bar' },
{ foo: 'foo', bar: 'foo' },
]).toContainEqual({ foo: 'foo', bar: 'foo'});
```
### Exepciones
- **toThrow**: Verifica si se lanza una excepción. La aserción toThrow la podemos ejecutar simplemente sin argumentos o pasándole como argumento un objeto "Error", para que, además de detectar la excepción en sí misma, detecte que se ha lanzado un objeto de este tipo. También podríamos verificar un mensaje de error exacto o evaluar una expresión regular.
```javascript=
it('Exceptions', () => {
const throwError = () => {
throw new Error('Something wrong...');
};
expect(throwError).toThrow();
expect(throwError).toThrow(Error);
expect(throwError).toThrow('wrong');
expect(throwError).toThrow(/wrong/);
});
```
## Aserciones Custom
La validación del comportamiento deseado debería ser muy explícita para que la intención de la persona que programó el test quede lo más clara posible. Si para validar un único comportamiento necesitamos varias líneas de aserciones, cosa que suele pasar con frecuencia cuando operamos con colecciones o con campos de objetos, a veces suele ser mejor crear un método propio en el cual las agrupemos creando nuestra propia aserción, sobre todo si esas líneas van juntas en varios test. Esto lo podemos hacer de dos maneras: o agrupando varias aserciones mediante una función propia o extendiendo la librería de aserciones mediante su API.
### Aserciones por agrupación
Agrupando varias aserciones mediante una función propia.
Por ejemplo, para validar que una lista contiene tres números en un cierto orden podríamos verificar el tamaño de la lista y luego el valor de cada uno de sus elementos.
```typescript=
test('custom assertions (grouping)', () => {
const list = [10, 20, 30];
expect(list.length).toBe(3);
expect(list[0]).toBe(10);
expect(list[1]).toBe(20);
expect(list[2]).toBe(30);
});
```
O bien, podríamos también ser más explícitos, tal y como hemos visto, y hacer uso de comparaciones más semánticas basadas en el contenido. En este caso, podríamos hacer uso del matcher toEqual.
```typescript=
test('custom assertions (grouping)', () => {
const list = [10, 20, 30];
expect(list).toEqual([10, 20, 30]);
});
```
Supongamos que Jest no nos permite ser tan explícitos y vamos a crear nuestra propia aserción mediante una función.
expectThatList es una función que recibe por parámetro un array de objetos de tipo T y devuelve, a su vez, una función que podría contener diferentes matchers.
En este caso, la función listMatchers solo va a contener la aserción isExactly. A su vez, isExactly puede recibir como parámetro n elementos de tipo T. Para ello, vamos a usar la sintaxis de los parámetros rest.
Lo primero que hacemos es comparar que el tamaño de las listas sean iguales y, a continuación, recorremos las dos listas mediante un foreach y las comparamos elemento a elemento usando el matcher de Jest toEquals.
```typescript=
test('custom assertions (grouping)', () => {
const list = [{ value: 10 }, { value: 20 }, { value: 30 }];
expectThatList(list).isExactly({ value: 10 }, { value: 20 }, { value: 30 });
});
function expectThatList<T>(list: T[]) {
return listMatchers(list);
}
function listMatchers<T>(list: T[]) {
return {
isExactly(...items: T[]) {
expect(items.length).toBe(list.length);
items.forEach((_, i) => {
expect(items[i]).toEqual(list[i]);
});
},
};
}
```
### Aserciones de Extensión
Otra opción para implementar aserciones propias es extender la librería de aserciones del framework siguiendo su misma filosofía. Jest dispone de una API específica para eso, a través del método extend de expect. Todos los matchers que incluyamos ahí deben devolver un objeto (o una promesa de un objeto, en el caso de ser asíncrono) con dos claves: pass y message.
Veamos un ejemplo: vamos a construir un custom toBe para que sea lo más sencillo posible. Esta aserción propia recibe uno o varios parámetros: el primer parámetro sería el valor esperado y el resto, valores recibidos.
En el campo pass debemos realizar la comparación para saber si hubo coincidencia o no, mientras que el campo message devolvemos una función sin argumentos que a su vez devuelve el mensaje de error en caso de falla.
```typescript=
expect.extend({
customToBe(expected, received) {
return {
pass: expected === received,
message: () => `Expected: ${expected} \nReceived: ${received}`,
};
},
});
test('custom assertion (extending)', () => {
const list = [10, 20, 30];
expect(list.length).customToBe(3);
});
```
Si lo hacemos fallar vemos que funciona prácticamente igual que la aserción toBe que trae el framework, pero sin los colores.
Para hacer los tipos compatibles debemos añadir nuestro customToBe a la interfaz de Matchers de Jest. Debido a que Jest no es un módulo importado tenemos, por así decirlo, que extender el namespace de su definición de tipos respetando su firma. Por cierto, el genérico de Matchers se llama R, porque ellos lo denominan así dentro de su definición y se refiere al valor recibido.
Los namespaces (o espacios de nombres) no es algo que se use comúnmente en TypeScript, más allá de la definición de tipos. Por esta razón el linter nos lo marca como un error. Para este caso, vamos a omitirlo.
```typescript=
declare namespace jest{
interface Matchers<R>{
customToBe(value): CustomMatcherResult;
}
}
```
*isExactly extendiendo Jest*
```typescript=
declare namespace jest{
interface Matchers<R>{
isExactly<T>(...values: T[]): CustomMatcherResult;
}
}
expect.extend({
isExactly<T>(expectedList: T[], ...values: T[]) {
const haveSameLength = expectedList.length === values.length;
return {
pass: haveSameLength,
message: () => `Expected: ${expectedList} \nReceived: ${values}`
};
}
});
test('custom assertion (extending)', () => {
const list = [10, 20, 30];
expect(list).isExactly(10, 20, 30);
});
```
[Custom Matchers](https://dev.to/zirkelc/improve-your-testing-with-custom-matchers-in-jest-2ibm)
## Mocks
En esencia, Jest dispone de tres elementos que nos permiten realizar simulaciones.
- **jest.fn**: simula un método o función.
- **jest.spyOn**: igual que jest.fn y, además, permite restaurar la implementación original. Aunque en el nombre incluye el prefijo spy, no solo puedes crear espías, también permite crear stubs. Es el más recomendado, ya que a diferencia de los anteriores mantiene por defecto la implementación original, además de mantener el TypeChecking. En este caso, si quisiéramos reemplazar la implementación original, por ejemplo, para crear un stub, tendríamos que hacerlo explícitamente:
```typescript
describe('The calculator', () => {
it("calls arithmetic add", () => {
const mockedAdd = jest.spyOn(arithmetic, 'add');
mockedAdd.mockImplementation(() => 10);
expect(calculator.doAdd(1, 2)).toBe(3);
expect(addMock).toHaveBeenCalledWith(1, 2);
});
it("calls arithmetic subtract", () => {
const mockedSubtract = jest.spyOn(arithmetic, 'subtract');
expect(calculator.doAdd(1, 2)).toBe(-1);
expect(addMock).toHaveBeenCalledWith(1, 2);
});
});
```
- **jest.mock**: simula un módulo entero, es decir, todo el contenido que tiene el fichero que se le pase. En la práctica no se suele usar mucho ya que por lo general nos suele interesar sustituir una cierta parte.
Principalmente, nos encontraremos con simulaciones de tipo stub y spy. Las implementaciones con mocks estrictos son menos comunes en Jest. Es importante aclarar que en Jest no se respeta la nomenclatura de Meszaros.
### Aserciones para mocks
Cualquier de los tres artefactos que nos ofrece Jest para realizar simulaciones los podemos validar con las siguientes aserciones:
- **toHaveBeenCalled()**: comprueba si el método simulado ha sido llamado, independientemente del número de veces.
- **toHaveBeenCalledTimes(1)**: es igual que el anterior, pero en este caso comprueba si ha sido llamado un número exacto de veces.
- **toHaveBeenCalledWith(1, 2)**: comprueba si ha sido llamado con los parámetros adecuados.
###### tags: `Jest` `JavaScript` `TypeScript` `Testing`