Aprende cómo aplicar Jest Mock paso a paso fácil y sin dolor

Aprende cómo aplicar Jest Mock paso a paso fácil y sin dolor

¿Quieres hacer una prueba automatizada en JavaScript pero tienes problemas para manejar dependencias con Jest Mock? Entonces haz llegado al tutorial de paso a paso para todo lo relacionado para hacer mocks con Jest.

Mira la tabla de contenidos a continuación y da click al tema en cuestión que necesitas resolver.

Tabla de contenidos

Qué es un Mock en Jest y ejemplos básicos

Qué es un mock Jest
Ejemplo de Mock en Jest

Un Mock en Jest es un objeto que imita la interfaz y propiedades de una función real, o una clase, o un módulo, o cualquier otro elemento de software, que puedes definir un comportamiento, almacena en memoria información sobre cómo ha sido utilizado y que sirve para propósitos de pruebas automatizadas.

Una manera de definir un mock en Jest es con jest.fn(), el cuál retorna un objeto de tipo "mock".

test('playing with mocks', () => {
  const mock = jest.fn()

  console.log(mock) //

  // es una función
  mock()

  // toHaveBeenCalled y toHaveBeenCalledTimes son matchers
  // que vienen por default en Jest y sirven sólo
  // para los mocks
  expect(mock).toHaveBeenCalled() // true
  expect(mock).toHaveBeenCalledTimes(2) // false
})

¿Para qué te puede servir esto?

Para entenderlo mejor, podemos comparar lo que es una función determinística (o función pura) y no determinística.

Una función determinística es aquella que dados los mismos inputs, siempre va a retornar el mismo output o resultado. Se llama de este modo porque su resultado está determinado por los inputs.

const sum = (a, b) => a + b

Esta función es determinista porque no importa qué pase, siempre que ejecutes sum(1, 2), el resultado será 3.

Si quisieras crear unit tests con Jest para esa simple función, sería muy fácil, bastaría con hacer algo como:

test('sum 1 + 2 should be equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

En cambio, la siguiente es una función no determinista:

const getRandomNumber = () => Math.random()

Esta función es no determinista porque tiene un cálculo dinámico cuyo resultado dependerá del momento en que será ejecutada.

¿Cómo harías un unit test que compruebe su resultado sin importar el momento en que lo ejecutes?

He aquí cuando usar mock puede servir de tal modo que podamos tener un absoluto control de las dependencias, como en este caso, Math.random().

En otras palabras, lo que nos ayuda a resolver los mocks es en poder reemplazar e imitar el comportamiento de dependencias en nuestro código pero sólo en tiempo de ejecución de las pruebas.

Puedes estar pensando "kha?". Deja te muestro un ejemplo:

const randomNumberExpected = 0.123456789

beforeEach(() => {
  // modificamos el comportamiento del método random
  // para que retorne randomNumberExpected
  jest.spyOn(global.Math, 'random').mockReturnValue(randomNumberExpected)
})

afterEach(() => {
  // eliminamos el comportamiento asignado anteriormente
  jest.spyOn(global.Math, 'random').mockRestore()
})

test('it should return a random value', () => {
  // pasa porque estamos controlando el comportamiento de Math.random
  expect(getRandomNumber()).toBe(randomNumberExpected)
  // otra posible asersión:
  expect(Math.random).toHaveBeenCalled()
})

Lo que estamos haciendo es que por medio de jest.spyOn, cuando se ejecute el método random del objeto global Math (usar global.Math o solamente Math son equivalentes en JS), siempre va a retornar randomNumberExpected, pero sólo cuando ejecutemos los tests con Jest.

Internamente Jest se va a encargar de modificar el comportamiento de Math.random en base a lo que hemos definido.

En la parte de afterEach lo que hacemos es simplemente restaurar el comportamiento original de Math.random.

La esencia de este ejemplo es que con Jest podemos reemplazar y modificar temporalmente el comportamiento de dependencias para nuestros fines. Controlamos las pruebas haciendo determinístico el resultado.

Pero eso no es todo, un mock también puede tener un historial de las veces que ha sido ejecutado y los parámetros que ha recibido, algo que probablemente puede ser más útil como cuando estás desarrollando una biblioteca open source.

Por cierto, si esto te parece confuso, no te preocupes, en ese caso te recomiendo que veas Fundamentos de testing en Javascript donde explico todo más desde cero.

En el mundo real, esto lo vas a aplicar cuando necesites hacer unit test a una app FE que consuma una API externa o como cuando tenga una API desarrollada con Node JS que se conecta a una DB para hacer una consulta.

Si quieres más información de Jest y testing en general tanto en el Frontend como en el Backend, Da click aquí para ver la lista de reproducción de vídeos en el canal de YouTube!

En resumen, un mock en Jest es un objeto que reemplaza una dependencia real en tiempo de ejecución de las pruebas, imita la misma interfaz y propiedades, y nos permite tener un historial de sus ejecuciones.

Las ventajas y desventajas de usar Mocks en tus pruebas

Una de las principales ventajas de usar mocks es que tus pruebas van a ser más rápidas porque vas a reemplazar una implementación real por una que es "fake".

Por otra parte, con este estilo de testing, vas a estar creando pruebas acopladas a los detalles de implementación.

Esto significa que si haces un refactor o un cambio en el código que estás probando, es muy probable que el test falle y tengas que actualizarlo.

Por ejemplo, si tenemos una función que consume una API con fetch:

function fetchData() {
  return fetch('some-api-url').then((postsResponse) => {
    if (postsResponse.ok) {
      return postsResponse.json()
    }
    return Promise.reject()
  })
}

export { fetchData }

¿Cómo la probarías?

Una buena práctica para pruebas unitarias es que deben ser independientes y aísladas. En este caso, una API es externa, incluso aunque sea una creada por nosotros mismos y seamos dueños.

Por lo tanto, es una buena práctica que nuestro unit test no consuma la API real.

Esto es así porque necesitamos tener el control, y si la API se cae o tiene errores, nuestro test va a fallar.

Una manera de resover esta dependencia externa es con un mock de jest, por ejemplo:

const theExpectedApiResponse = {} // lo que sea que esperemos de la API

// reemplazamos el objeto global fetch por un mock
// el mock es creado con jest.fn() y
// estamos definiendo el comportamiento esperado en el callback que pasamos
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve(theExpectedApiResponse),
  })
)

it('returns the api data expected', async () => {
  const data = await fetchData()

  // ya que controlamos fetch, podemos asegurar que retorna lo
  // que definimos en el comportamiento del mock
  expect(data).toEqual(theExpectedApiResponse)

  // podemos revisar también que se haya ejecutado N veces y cómo
  expect(fetch).toHaveBeenCalledTimes(1)
  expect(fetch).toHaveBeenCalledWith('some-api-url')
})

Esto a primera instancia se ve cool, estamos creando un unit test muy granular, independiente de si esta función la estemos usando en un FE con React JS o en un BE con Node JS.

Pero la gran desventaja es que estamos acoplando la prueba con el detalle de implementación, lo cuál significa que si refactorizamos fetchData usando axios en lugar de fetch, nuestra prueba ya no va a pasar 😱

function fetchData() {
  return axios.get('some-api-url')
}

export { fetchData }

Entonces vamos a necesitar actualizar la prueba también, lo que es un trabajo extra.

Otra desventaja es que también reduciremos el code coverage, aunque en mi opinión, es la menor de mis preocupaciones debido a que el code coverage por sí sólo no significa que tengamos buenos tests.

Code coverage es una métrica utilizada para medir cuántas líneas de código están siendo validadas por nuestros tests.

En mi experiencia, he notado que la mayoría de los desarrolladores (incluso hasta "seniors"), no les gustan las pruebas automatizadas porque sólo conocen aquellas que están acopladas a los detalles de implementación y que son difíciles de mantener en consecuencia.

¿Esto significa que nunca debas usar mock? No lo creo. Pienso que va a depender del caso de uso.

Por ejemplo, si estás desarrollando una biblioteca open source, es posible que en algunas partes quieras tener pruebas granulares donde revises detalles de implementación.

Explico esto y más cosas a mayor detalle en base a experiencia de trabajar en proyectos de la vida real en los vídeos de esta lista de reproducción.

Cómo hacer Mock de modules o módulos

Jest nos provee de jest.mock('module') como una manera de crear un mock de un módulo entero. Por módulo se entiende una biblioteca instalada en tu proyecto (como con npm) o un archivo de tu código fuente. Por ejemplo:

jest.mock('uuid') // dependencia instalada

// o un archivo local
jest.mock('../path/file')

Esto nos permite hacer pruebas de funciones que estén siendo exportadas por el archivo local en cuestión o en dependencias.

Vamos a ver ejemplos concretos para hacer mock de funciones, clases de ES6 y más en los siguientes apartados.

Cómo hacer Mock de function o funciones

Jest nos permite hacer mock de function o funciones de las maneras siguientes:

  • jest.fn(). Retorna un objeto de tipo Mock.
  • jest.mock('module', () => interfaz). Crea un mock de un módulo y en el callback defines la interfaz (nombres de funciones, sus parámetros y lo que quieras que retornen).
  • jest.spy(object, property, interfaz). Haces un spy a la propiedad de un objeto y puedes determinar la interfaz y el comportamiento del mismo.

Veamos algunos ejemplos.

// user-model.js
export const getUser = (userId) => {
  // code...
}

export const createUser = (userData) => {
  // code...
}

export const updateUser = (userId, userData) => {
  // code...
}

export const deleteUserBy = (userId) => {
  // code...
}

Y lo utilizas en el siguente controlador:

// user-controller.js
import { getUser, createUser, updateUser, deleteUserBy } from './model-user'

export const getUserController = (userId) => {
  // code... getUser...
}

export const createUserController = (userData) => {
  // code... createUser...
}

export const updateUserController = (userId, userData) => {
  // code... updateUser...
}

export const deleteUserByController = (userId) => {
  // code... deleteUserBy...
}

Si quisieras crear pruebas para user-controller, necesitas resolver el tema de las dependencias con user-model.

Pienso que existen 3 posibilidades:

  1. Hacer un mock del módulo user-model para que en las pruebas de user-controller no ejecute realmente las acciones del modelo, como crear un usuario.
  2. No hacer mock de user-model, pero sí un mock de lo que usa internamente dicho módulo, por ejemplo, si usa mysql, mongodb, o cualquier otra base de datos u ORM para hacer operaciones.
  3. No hacer mock ni de user-model ni del módulo o dependencia usada para las operaciones a nivel base de datos, sino más bien hacer un mock de la base de datos en sí misma, como una base de datos en memoria que existirá en tiempo de ejecución del test. Si quieres ver un ejemplo de esto último entonces mira este post donde lo hago con Node JS, Express y MongoDB aplicando TDD.

En este ejemplo, vamos a hacer el primer punto porque si estás leyendo esto es porque te interesa aprenderlo con mock 😆

Entonces el test para user-controller sería como:

import { getUserController } from './user-controller'
// createUser automáticamente es de tipo mock
// en virtud de jest.mock('./user-model')
import { getUser } from './user-model'

console.log(getUser) // Mock object

// asumiendo que los archivos están en la misma ubicación
jest.mock('./user-model')

beforeEach(() => {
  // hago reset del comportamiento que haya configurado dentro
  // de cada test, para que no afecte en otras pruebas
  // de este mismo archivo
  getUser.mockClear()
})

test('get a current user', () => {
  const expectedUser = { name: 'john galt' }
  const expectedId = 1

  // getUser es un mock que tiene la función
  // returnValueOnce para asignar lo que queremos
  // que retorne cuando se ejecute como función
  getUser.returnValueOnce(expectedUser)

  const user = getUserController(expectedId)

  expect(user).toEqual(expectedUser)
  expect(getUser).toHaveBeenCalledWith(expectedId)
})

// resto de pruebas...

Lo que estamos haciendo aquí es reemplazar la función getUser por un objeto mock de Jest por medio de jest.mock('./user-model'), es por ello que cuando lo importo, si le hago un console log, me va a indicar que es un Mock.

Hago un getUser.mockClear() antes de ejecutar cada prueba para poder remover cualquier comportamiento o historial interno del mock en cuestión (la cantidad de veces que se ha ejecutado por ejemplo).

Cómo hacer Mock de ES6 class o clases

Una clase en ES6 luce del siguiente modo:

class User {
  name = 'john'

  getGreeting() {
    return `hello ${this.name}`
  }
}

// usar como:
const user = new User()
user.getGreeting()

Sin embargo, podemos tener estructuras más complejas en las que tengamos una clase que se puede componer por instancias de otras clases:

// Greeting.js
class Greeting {
  greet(name) {
    return `hello ${name}`
  }
}

export default Greeting
// User.js
import Greeting from './Greeting'

class User {
  greeting = new Greeting()
  name = 'john'

  getGreeting() {
    return this.greeting.greet(this.name)
  }
}

export default User
// usar como:
const user = new User()
user.getGreeting()

Esto es llamado como composición de clases que es una alternativa de la herencia de clases para reutilziar lógica o comportamiento.

En el mundo real puedes notar esta "técnica" aplicada en una infinidad de escenarios: una clase que tenga reglas de negocio, otra clase que represente a entidades concretas, y así una variedad de tipos.

Entonces puede ser deseable crear pruebas unitarias a una clase que tenga como propiedades, instancias de otras clases, o incluso que las herede.

Vamos a ver diferentes ejemplos de cómo hacer una prueba unitaria para la clase User haciendo un mock de Greeting.

Mock automático con jest.mock()

// Greeting.test.js

import Greeting from './Greeting'

jest.mock('./Greeting')

test('User greeting', () => {
  const user = new User()

  user.getGreeting()

  // greeting.greet es un mock
  expect(user.greeting.greet).toHaveBeenCalledWith(user.name)
})

Ahora quizá te estés preguntando, ¿Por qué querría hacer eso? a lo que te contesto: ¿Por qué alguien querría?

No soy partidario de hacer pruebas acopladas a detalles de implementación como en este caso porque conducen a que sean muy frágiles a los cambios y es doble mantenimiento, además de que no creo que nos aporten realmente mucho valor.

Esta diferencia de opinión es debido a que existen diferentes enfoques al hacer pruebas automatizadas.

Pero como es una duda común en la comunidad, decidí incluirlo 😉

Cómo hacer Mock de methods o métodos

Cómo hacer Mock de promises o promesas

Hacer un mock a funciones que retornen promesas es muy sencillo. La clave está en definir ese comportamiento en el mock y ya está. Veamos un ejemplo.

Digamos que tenemos la lógica de llamada a una Api en una capa de abstracción independiente, ya sea que la usemos en el FE o en el BE, puedes hacer esa separación.

Re utilizando el ejemplo de fetch:

// fetch-data.js
function fetchData() {
  return fetch('some-api-url').then((postsResponse) => {
    if (postsResponse.ok) {
      return postsResponse.json()
    }
    return Promise.reject(new)
  })
}

export { fetchData }

Y la utilizamos en un archivo intermedio, que podría ser un controlador en un BE usando el patrón MVC (un BE puede hacer fetch a otras Apis), o bien puede ser un action asíncrona de Redux en el FE, por decir algunos ejemplos reales.

// some-consumer-file.js
import { fetchData } from './fetch-data'

export const getData = async () => {
  try {
    const data = await fetchData()
    return data
  } catch (error) {
    return error.message
  }
}

Si por alguna razón quisieras hacer un unit test de some-consumer-file.js, podrías hacer:

  1. Un mock de todo el módulo fetch-data.js.
  2. Hacer mock de la dependencia fetch y dejar el módulo de fetch-data.js normal.

Para efectos prácticos, vamos a hacer el segundo caso.

Notamos que fetchData es una función que a su vez, retorna una promesa, por lo tanto, si hacemos un mock de esto también necesitamos añadir el comportamiento de que retorne una promesa como tal.

// some-consumer-file.test.js
import { getData } from './some-consumer-file.test'

const response = {} // lo que queramos asignar

global.fetch = jest.fn(() =>
  Promise.resolve({
    // la clave está en Promise.resolve
    json: () => Promise.resolve(response),
  })
)

// resetear
beforeEach(() => {
  fetch.mockClear()
})

test('return the data successfully', async () => {
  const data = await getData()

  expect(data).toEqual(response)
  // detalle de implementación
  expect(fetch).toHaveBeenCalledTimes(1)
})

La parte de Promise.resolve es parte nativa de JS y según esta documentación de mozilla, lo que hace es:

retorna un objeto Promise que es resuelto con el valor dado

Básicamente, equivale a:

const response = {} // lo que queramos asignar

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => new Promise((resolve) => resolve(response)),
  })
)

Pero es mucho más simple colocar solamente Promise.resolve(response).

De esta manera podemos hacer mocks que tengan promesas 😉.

Si quisieras hacer una prueba para un escenario donde maneje errores, es igualmente aplicable el Promise.reject(New Error('error test scenario')):

// some-consumer-file.test.js
import { getData } from './some-consumer-file.test'

const response = {} // lo que queramos asignar

global.fetch = jest.fn(() =>
  Promise.resolve({
    // la clave está en Promise.resolve
    json: () => Promise.resolve(response),
  })
)

// resetear
beforeEach(() => {
  fetch.mockClear()
})

test('return the data successfully', async () => {
  const data = await getData()

  expect(data).toEqual(response)
  // detalle de implementación
  expect(fetch).toHaveBeenCalledTimes(1)
})

test('handle errors successfully', async () => {
  const expectedErrorMessage = 'test error scenario'
  // sólo para este test
  fetch.mockImplementationOnce(() => Promise.reject(errorMessage))

  const response = await getData()

  expect(response).toBe(expectedErrorMessage)
  // detalle de implementación
  expect(fetch).toHaveBeenCalledTimes(1)
})

Y con esto lo podemos manejar también.

Cómo hacer Mock de axios

La manera más fácil de hacer un mock con axios es haciendo un mock automático por medio de jest.mock como sigue a continuación:

// fetch-data.js
async function fetchData() {
  try {
    return await axios.get('some-endpoint')
  } catch (error) {
    return error.response
  }
}

export { fetchData }
// fetchData.test.js
import axios from 'axios'

import { fetchData } from './fetchData'

jest.mock('axios')

// resetamos axios.get antes de cada test
// para que cada test sea independiente
// y no afecten los cambios de estado
beforeEach(() => {
  axios.get.mockClear()
})

test('success GET scenario', () => {
  // lo que quieras que retorne
  const expectedData = {}
  // asignamos comportamiento deseado para este test
  axios.get.mockResolvedValueOnce(Promise.resolve(expectedData))

  const response = await fetchData()

  // toEqual es mejor para comparar estructuras como objetos
  expect(response).toEqual(expectedData)
})

test('error GET scenario', () => {
  // lo que quieras que retorne
  const expectedErrorData = { errorMessage: 'test error scenario' }
  // asignamos comportamiento deseado para este test
  axios.get.mockResolvedValueOnce(Promise.reject(expectedErrorData))

  const response = await fetchData()

  // toEqual es mejor para comparar estructuras como objetos
  expect(response).toEqual(expectedErrorData)
})

De esta manera es mucho más sencilla.

Cómo hacer Mock de de enviroment variables o variables de entorno

La manera más sencilla de hacer tests con Jest haciendo mock de variables de entorno es configurando manualmente dichas variables con JS y ejecutando esa lógica en el archivo de configuración de Jest:

// setup-env-vars.js

const customEnvVars = {
  VAR_NAME: 'varValue',
  VAR_NAME_2: 'varValue2',
}

process.env = { ...process.env, ...customEnvVars }

Y llamarlo en el archivo de setup de los tests configurado en Jest;

// setupTests.js
require('./setup-env-vars.js')

// rest of the code...

Recuerda que necesitas indicarle a Jest que ejecute este archivo de configuración en caso de que no lo tenga:

// jest.config.js
module.exports = {
  setupFiles: ['<rootDir>/.jest/setupTests.js'],
}

Y listo!

Cómo hacer Mock de window

La manera más fácil de hacer mock de window es por medio de un spy:

// variable global en este archivo de test
let windowSpy

// asignamos windowSpy igual al mock que retorna jest.spyOn
beforeEach(() => {
  windowSpy = jest.spyOn(window, 'window', 'get')
})

// reseteamos windowSpy
afterEach(() => {
  windowSpy.mockRestore()
})

// en caso de que quieras modificar el window.location.origin
it('should return https://example.com', () => {
  windowSpy.mockImplementation(() => ({
    location: {
      origin: 'https://example.com',
    },
  }))

  expect(window.location.origin).toEqual('https://example.com')
})

Y con esto basta ;D

¿Qué otras alternativas hay de hacer mock con Jest? Y recursos recomendados

Si bien existe la opción de usar mocks de Jest, como vimos en los pros y contras, un aspecto negativo es que normalemnte tiende a que acoplemos nuestras pruebas a detalles de implementación, lo cuál nos puede afectar negativamente en el mantenimiento de las mismas.

En los escenarios en los que consumimos una API, en lugar de hacer un mock de axios, de fetch o del módulo en cuestión donde tengamos la lógica, más bien podemos hacer un mock del BE como tal con herramientas como Mock Service Worker.

Por ejemplo, puedes ver el siguiente vídeo donde lo explico:

Te recomiendo que des un ojo a lo siguiente:

juan correa
¿Quieres pasar a nivel Senior en React?
Soy Juan Correa y he ayudado a cientos de desarrolladores a avanzar en sus carreras ¿Quieres saber cómo?