Introducción al Mocking con Jest

Introducción al Mocking con Jest

Jest es un popular framework de testing para JavaScript, conocido por su facilidad de uso y su robusto soporte para la técnica de mocking. He notado que uno de los artículos más visitados en este blog es sobre "Entendiendo Code Covereage Usando Jest", así es que pensando en que están interesados en las pruebas de sus aplicaciones, he escríto este artículo en el cual exploraremos cómo usar Jest para hacer mocking de funciones, módulos, aparte de conocer los beneficios que esto nos brinda en las pruebas y algunas desventajas a tener en cuenta porque siempre hay puntos negativos a considerar.

¿Qué es Jest?

Jest fue creado por Facebook inicialmente para React y fue creado pensando en la simplicidad porque supongo que Mocha, Jasmine, Nightwatch y otros no eran lo suficientemente sencillos para trabajar.

Entre sus características principales se incluyen:

  • Fácil configuración
  • Aserciones legibles
  • Mocking automático
  • Ejecución tests en paralelo

¿Pero qué es el Mocking?

El mocking consiste en crear una implementación falsa de una dependencia que luego usaremos en nuestras pruebas. Las dependencias son partes externas de nuestro código que importamos para utilizar su funcionalidad.

En vez de llamar al código real de la dependencia, Jest permitirá que llamemos a la versión "mockeada" lo cual es útil por varias razones:

  • Nos permite probar nuestro código en aislamiento sin ejecutar el código real de las dependencias.
  • Podemos simular cualquier comportamiento deseado de la dependencia que hemos hecho mock para probar diferentes escenarios.
  • Los mocks son rápidos de ejecutar porque no hay ningún código real detrás.

Básicamente, el mocking nos da mayor control sobre el entorno de testing y facilita probar componentes de forma aislada.

Haciendo mocking de funciones

Podemos usar jest.fn() para hacer mock de una función y esto nos devuelve una versión mock de la función, de la cual podemos controlar su resultado.

Por ejemplo:

const mockFunc = jest.fn();

// Llamamos la función que hicimos mocking
mockFunc(); 
// -> undefined

// Y por ejemplo, podemos simular un return
mockFunc.mockReturnValue('Jest');

// Entonces al llamarla la funcion, devolvería 'Jest'
mockFunc();
// -> 'Jest'

También podemos simular llamadas asíncronas en caso de que estés utilizando Promises en tus funciones y para ellos Jest cuenta con los métodos .mockResolvedValue y .mockRejectedValue.

Incluso, los mocks nos permiten verificar cuántas veces se llamó una función y con qué argumentos:

// Imagina que mockFunc se llamó 2 veces  
// una con 'a' y 'b'
// y otra con 'c' y 'd'

// y al momento de hacer una aserción, podrías utilizar lo siguiente
mockFunc.mock.calls; 
// -> [['a', 'b'], ['c', 'd']]
// verificando los valores a lo largo de las llamadas

Más o menos claro cierto? veamos un ejemplo más avanzado:

// users.js
export async function getUser(id) {
  // hace una petición HTTP para obtener el usuario 
}

jest.mock('./users'); 

// users.test.js
import * as users from './users';

test('maneja error en getUser', async () => {
  users.getUser.mockRejectedValue(new Error('Error al obtener usuario'));

  try {
    await users.getUser(1); 
  } catch (e) {
    expect(e.message).toBe('Error al obtener usuario'); 
  }
})

Haciendo mocking de módulos

Podemos usar jest.mock() para hacer mocking de un módulo entero (archivos externos que retornan objetos o métodos.

Por ejemplo:

// math.js
export function sum(a, b) {
  return a + b; 
}
jest.mock('./math'); 

// math.test.js
import * as math from './math';

test('math.sum version mock', () => {
  math.sum.mockReturnValue(10); 

  expect(math.sum(1, 2)).toBe(10);
})

Y no solamente podemos hacer mocking de un solo archivo, sino que podemos utilizarlo para mocking de múltiples módulos utilizando jest.mock('./module', () => { /* custom mocks */ }).

Por Ejemplo:

Imagina que tenemos dos módulos, math.js y strings.js:

// math.js
export function sum(a, b) {
  return a + b;
}
// strings.js
export function capitalize(str) {
  return str[0].toUpperCase() + str.slice(1); 
}

En nuestro archivo de pruebas, podemos mockear ambos módulos de la siguiente manera:

// test.js

jest.mock('./math.js', () => {
  return { 
    sum: jest.fn(() => 42) 
  };
});

jest.mock('./strings.js', () => {
  return {
    capitalize: jest.fn(str => str.toLowerCase())  
  };
});

// Ahora con un mock de ambos módulos

import * as math from './math'; 
import * as strings from './strings';

test('mockea múltiples módulos', () => {
  expect(math.sum(1, 2)).toBe(42);
  expect(strings.capitalize('HELLO')).toBe('hello'); 
});

Pasamos un callback a jest.mock que customiza el mock para cada módulo. De esta forma podemos mockear y controlar el comportamiento de múltiples dependencias de manera simple y organizada.

Haciendo mocking de dependencias externas

Los mocks también son muy útiles para aislar nuestro código de dependencias externas que no podemos controlar directamente.

Por ejemplo, imaginemos que tenemos un servicio que depende de una API externa:

// api.js
import axios from 'axios';

export async function getUser(id) {
  const response = await axios.get(`https://api.example.com/users/${id}`);
  return response.data;
}

// usersService.js 
import { getUser } from './api';

export async function getUserName(userId) {
  const user = await getUser(userId);
  return user.name; 
}

Podemos hacer mocking de getUser para simular cualquier respuesta sin hacer realmente las llamadas a la API:

jest.mock('./api');

// usersService.test.js
import { getUserName } from './usersService';
import * as api from './api';

test('getUserName', async () => {
  const mockUser = { name: 'John' };
  api.getUser.mockResolvedValue(mockUser);

  const user = await getUserName(123);
  expect(user).toBe('John'); 
});

IMPORTANTE: El orden en que hacemos el mocking y las importaciones sí puede ser importante. Por defecto, cuando llamamos a jest.mock('./module') se mockea el módulo antes de la primera importación.

Así que esto funcionaría:

// mock antes de importar 
jest.mock('./strings');

import * as strings from './strings';

Pero si importamos antes, el mocking no tendrá efecto:

// falla porque ya se importó

import * as strings from './strings'; 

jest.mock('./strings');

Para que funcione el mocking después de la importación, necesitamos pasar una opción:

// fuerza el mocking después de la importación
jest.mock('./strings', () => {}, { virtual: true });

El flag virtual: true hace que Jest reemplace el módulo al que se hizo mocking incluso si ya fue importado.

Conclusión

Creo que con estos ejemplos y explicaciones ya podría quedar claro como se utiliza Jest para hacer mocking de funciones y módulos, es realmente sencillo y obviamente se requiere de mucha práctica para determinar que cosas si o no hacer mocking, considera siempre tener cuidado de no sobreutilizar mocks y probar suficiente código real, de lo contrario tus pruebas no serían lo suficientemente válidas. Y como dicen, la práctica hace al maestro.

Happy coding! :D


Introducción al Mocking con Jest

Jest es un popular framework de testing para JavaScript, conocido por su facilidad de uso y su robusto soporte para la técnica de mocking. He notado que uno de los artículos más visitados en este blog es sobre "Entendiendo Code Covereage Usando Jest", así es que pensando en que están interesados en las pruebas de sus aplicaciones, he escríto este artículo en el cual exploraremos cómo usar Jest para hacer mocking de funciones, módulos, aparte de conocer los beneficios que esto nos brinda en las pruebas y algunas desventajas a tener en cuenta porque siempre hay puntos negativos a considerar.

¿Qué es Jest?

Jest fue creado por Facebook inicialmente para React y fue creado pensando en la simplicidad porque supongo que Mocha, Jasmine, Nightwatch y otros no eran lo suficientemente sencillos para trabajar.

Entre sus características principales se incluyen:

  • Fácil configuración
  • Aserciones legibles
  • Mocking automático
  • Ejecución tests en paralelo

¿Pero qué es el Mocking?

El mocking consiste en crear una implementación falsa de una dependencia que luego usaremos en nuestras pruebas. Las dependencias son partes externas de nuestro código que importamos para utilizar su funcionalidad.

En vez de llamar al código real de la dependencia, Jest permitirá que llamemos a la versión "mockeada" lo cual es útil por varias razones:

  • Nos permite probar nuestro código en aislamiento sin ejecutar el código real de las dependencias.
  • Podemos simular cualquier comportamiento deseado de la dependencia que hemos hecho mock para probar diferentes escenarios.
  • Los mocks son rápidos de ejecutar porque no hay ningún código real detrás.

Básicamente, el mocking nos da mayor control sobre el entorno de testing y facilita probar componentes de forma aislada.

Haciendo mocking de funciones

Podemos usar jest.fn() para hacer mock de una función y esto nos devuelve una versión mock de la función, de la cual podemos controlar su resultado.

Por ejemplo:

const mockFunc = jest.fn();

// Llamamos la función que hicimos mocking
mockFunc(); 
// -> undefined

// Y por ejemplo, podemos simular un return
mockFunc.mockReturnValue('Jest');

// Entonces al llamarla la funcion, devolvería 'Jest'
mockFunc();
// -> 'Jest'

También podemos simular llamadas asíncronas en caso de que estés utilizando Promises en tus funciones y para ellos Jest cuenta con los métodos .mockResolvedValue y .mockRejectedValue.

Incluso, los mocks nos permiten verificar cuántas veces se llamó una función y con qué argumentos:

// Imagina que mockFunc se llamó 2 veces  
// una con 'a' y 'b'
// y otra con 'c' y 'd'

// y al momento de hacer una aserción, podrías utilizar lo siguiente
mockFunc.mock.calls; 
// -> [['a', 'b'], ['c', 'd']]
// verificando los valores a lo largo de las llamadas

Más o menos claro cierto? veamos un ejemplo más avanzado:

// users.js
export async function getUser(id) {
  // hace una petición HTTP para obtener el usuario 
}

jest.mock('./users'); 

// users.test.js
import * as users from './users';

test('maneja error en getUser', async () => {
  users.getUser.mockRejectedValue(new Error('Error al obtener usuario'));

  try {
    await users.getUser(1); 
  } catch (e) {
    expect(e.message).toBe('Error al obtener usuario'); 
  }
})

Haciendo mocking de módulos

Podemos usar jest.mock() para hacer mocking de un módulo entero (archivos externos que retornan objetos o métodos.

Por ejemplo:

// math.js
export function sum(a, b) {
  return a + b; 
}
jest.mock('./math'); 

// math.test.js
import * as math from './math';

test('math.sum version mock', () => {
  math.sum.mockReturnValue(10); 

  expect(math.sum(1, 2)).toBe(10);
})

Y no solamente podemos hacer mocking de un solo archivo, sino que podemos utilizarlo para mocking de múltiples módulos utilizando jest.mock('./module', () => { /* custom mocks */ }).

Por Ejemplo:

Imagina que tenemos dos módulos, math.js y strings.js:

// math.js
export function sum(a, b) {
  return a + b;
}
// strings.js
export function capitalize(str) {
  return str[0].toUpperCase() + str.slice(1); 
}

En nuestro archivo de pruebas, podemos mockear ambos módulos de la siguiente manera:

// test.js

jest.mock('./math.js', () => {
  return { 
    sum: jest.fn(() => 42) 
  };
});

jest.mock('./strings.js', () => {
  return {
    capitalize: jest.fn(str => str.toLowerCase())  
  };
});

// Ahora con un mock de ambos módulos

import * as math from './math'; 
import * as strings from './strings';

test('mockea múltiples módulos', () => {
  expect(math.sum(1, 2)).toBe(42);
  expect(strings.capitalize('HELLO')).toBe('hello'); 
});

Pasamos un callback a jest.mock que customiza el mock para cada módulo. De esta forma podemos mockear y controlar el comportamiento de múltiples dependencias de manera simple y organizada.

Haciendo mocking de dependencias externas

Los mocks también son muy útiles para aislar nuestro código de dependencias externas que no podemos controlar directamente.

Por ejemplo, imaginemos que tenemos un servicio que depende de una API externa:

// api.js
import axios from 'axios';

export async function getUser(id) {
  const response = await axios.get(`https://api.example.com/users/${id}`);
  return response.data;
}

// usersService.js 
import { getUser } from './api';

export async function getUserName(userId) {
  const user = await getUser(userId);
  return user.name; 
}

Podemos hacer mocking de getUser para simular cualquier respuesta sin hacer realmente las llamadas a la API:

jest.mock('./api');

// usersService.test.js
import { getUserName } from './usersService';
import * as api from './api';

test('getUserName', async () => {
  const mockUser = { name: 'John' };
  api.getUser.mockResolvedValue(mockUser);

  const user = await getUserName(123);
  expect(user).toBe('John'); 
});

IMPORTANTE: El orden en que hacemos el mocking y las importaciones sí puede ser importante. Por defecto, cuando llamamos a jest.mock('./module') se mockea el módulo antes de la primera importación.

Así que esto funcionaría:

// mock antes de importar 
jest.mock('./strings');

import * as strings from './strings';

Pero si importamos antes, el mocking no tendrá efecto:

// falla porque ya se importó

import * as strings from './strings'; 

jest.mock('./strings');

Para que funcione el mocking después de la importación, necesitamos pasar una opción:

// fuerza el mocking después de la importación
jest.mock('./strings', () => {}, { virtual: true });

El flag virtual: true hace que Jest reemplace el módulo al que se hizo mocking incluso si ya fue importado.

Conclusión

Creo que con estos ejemplos y explicaciones ya podría quedar claro como se utiliza Jest para hacer mocking de funciones y módulos, es realmente sencillo y obviamente se requiere de mucha práctica para determinar que cosas si o no hacer mocking, considera siempre tener cuidado de no sobreutilizar mocks y probar suficiente código real, de lo contrario tus pruebas no serían lo suficientemente válidas. Y como dicen, la práctica hace al maestro.

Happy coding! :D


Photo by Dulkimso Hakim Santoso on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?