Al probar código JavaScript usando Jest, a veces puede que necesite simular un módulo. Ya sea porque el módulo o las funciones que exporta son irrelevantes para la prueba específica, o porque necesitas evitar que algo como una solicitud de API intente acceder a un recurso externo, un mock es increíblemente útil. Sin embargo, existen varios enfoques diferentes para hacer mocks de módulos en Jest, lo que puede generar confusión. ¿Cuál es el enfoque adecuado para cualquier escenario dado?


En este artículo, analizaremos varios escenarios utilizando módulos ES6 con exportaciones con nombre, una exportación predeterminada o una combinación de ambas.

ES6 Module Exports

Los módulos ES6 proporcionan dos formas diferentes de exportar métodos y variables desde un archivo: exportaciones con nombre (named exports) y exportaciones predeterminadas (default exports). Cualquier archivo dado podría tener una o más exportaciones con nombre, una exportación predeterminada o ambas exportaciones con nombre y una exportación predeterminada.


La forma en que simule su módulo en Jest dependerá de la forma en que se exporten los datos desde el módulo.

Escenarios de simulación del módulo

Al probar un módulo en Jest, existen varios escenarios posibles de simulación de módulos con los que puede encontrarse:

  • Sin necesidad de simular nada
  • Simular automáticamente del módulo
  • Simular el módulo usando el método de fábrica de módulos
  • Simular el módulo utilizando el método de fábrica de módulos e implementaciones simuladas
  • Parcialmente simular algunos métodos en el módulo pero no de todos los métodos

Exploremos cada una de estas posibilidades a continuación.

Simulando named exports

Primero, consideremos cómo probaríamos un módulo que solo exporta exportaciones con nombre. Comenzaremos con un archivo utils.js ficticio que contiene tres métodos que se exportan como exportaciones con nombre:

export const metodo1 = () => 'Este es el método 1'
export const metodo2 = () => 'Este es el método 2'
export const metodo3 = () => 'Este es el método 3'

Si tuviéramos que probar estos métodos exactamente como son sin necesidad de simular nada, nuestro archivo de prueba se vería así:

import { metodo1, metodo2, metodo3 } from './utils.js'
describe('named exports - sin simular nada', () => {
  it('devuelve el mensaje correcto para método 1', () => {
    expect(metodo1()).toBe('Este es el método 1')
  })
  it('devuelve el mensaje correcto para método 2', () => {
    expect(metodo2()).toBe('Este es el método 2')
  })
  it('devuelve el mensaje correcto para método 3', () => {
    expect(metodo3()).toBe('Este es el método 3')
  })
})

Si quisiéramos simular estos métodos usando la simulación automática, simplemente podríamos pasar la ruta del archivo al método jest.mock.


Nota: En estos ejemplos, vamos a escribir pruebas para verificar que el comportamiento de la simulación funcione correctamente. Se trata de algo como “meta-pruebas”, en las que probablemente no necesitaría probar que Jest se está comportando correctamente. En un escenario de prueba real, es probable que se esté simulando un módulo que consume un segundo módulo, donde los métodos del primer módulo no son relevantes para lo que está tratando de probar en el segundo módulo.

import { metodo1, metodo2, metodo3 } from './utils.js'
jest.mock('./utils.js')
describe('named exports - simulacion automatica que no devuelve nada', () => {
  it('devuelve el mensaje correcto para método 1', () => {
    expect(metodo1()).not.toBe('Este es el método 1')
    expect(metodo1()).toBe(undefined)
  })
  it('devuelve el mensaje correcto para método 2', () => {
    expect(metodo2()).not.toBe('Este es el método 2')
    expect(metodo2()).toBe(undefined)
  })
  it('devuelve el mensaje correcto para método 3', () => {
    expect(metodo3()).not.toBe('Este es el método 3')
    expect(metodo3()).toBe(undefined)
  })
})

Puede ver que para cada método, el valor de retorno real se reemplaza por un valor de retorno indefinido. Eso es porque automáticamente estamos simulando el módulo usando esta declaración: jest.mock ('./ utils.js').

Ahora, ¿qué pasaría si quisiéramos tener más control sobre cómo se simula cada método? En ese caso, podemos usar el método jest.mock junto con un método de fábrica de módulos como este:

import { metodo1, metodo2, metodo3 } from './utils.js'
jest.mock('./utils.js', () => ({
  metodo1: () => 'Este es el método 1 simulado',
  metodo2: () => 'Este es el método 2 simulado',
  metodo3: () => 'Este es el método 3 simulado',
}))
describe('named exports - modulo simulado con metodo de factory', () => {
  it('devuelve el mensaje correcto para método 1', () => {
 Este es el método 1 simulado')
    expect(() => expect(metodo1).toHaveBeenCalledTimes(1)).toThrow()
  })
  it('devuelve el mensaje correcto para método 2', () => {
    expect(metodo2()).toBe('Este es el método 2 simulado')
    expect(() => expect(metodo2).toHaveBeenCalledTimes(1)).toThrow()
  })
  it('devuelve el mensaje correcto para método 3', () => {
    expect(metodo3()).toBe('Este es el método 3 simulado')
    expect(() => expect(metodo3).toHaveBeenCalledTimes(1)).toThrow()
  })
})

Como puede ver, ahora hemos establecido explícitamente lo que debe hacer cada uno de nuestros métodos simulados. Devuelven el valor que les hemos establecido. Sin embargo, estas no son verdaderas funciones simuladas o “espías” todavía, porque no podemos espiar cosas como si se ha llamado o no a una función determinada.

Si quisiéramos poder espiar cada una de nuestras funciones simuladas, necesitaríamos usar la fábrica de módulos junto con una implementación simulada para cada función como esta:

import { metodo1, metodo2, metodo3 } from './utils.js'
jest.mock('./utils.js', () => ({
  method1: jest.fn().mockImplementation(() => 'Este es el método 1 simulado'),
  metodo2: jest.fn().mockImplementation(() => 'Este es el método 2 simulado'),
  method3: jest.fn().mockImplementation(() => 'Este es el método 3 simulado'),
}))
describe('named exports - implementacion simulada', () => {
  it('devuelve el mensaje correcto para método 1', () => {
    expect(metodo1()).toBe('Este es el método 1 simulado')
    expect(metodo1).toHaveBeenCalledTimes(1)
  })
  it('devuelve el mensaje correcto para método 2', () => {
    expect(metodo2()).toBe('Este es el método 2 simulado')
    expect(metodo2).toHaveBeenCalledTimes(1)
  })
  it('devuelve el mensaje correcto para método 3, () => {
    expect(metodo3()).toBe('Este es el método 3 simulado')
    expect(metodo3).toHaveBeenCalledTimes(1)
  })
})

Como puede ver, al utilizar el método jest.fn() para crear una función simulada y luego definir su implementación usando el método mockImplementation, podemos controlar lo que hace la función y espiarla para ver cuántas veces fue llamada.

Finalmente, si solo queremos simular algunos de los métodos pero no todos, podemos usar el método jest.requireActual para incluir las exportaciones de módulos reales en nuestro archivo de prueba. Por ejemplo, aquí simulamos la función metodo3 pero no de las funciones metodo1 o metodo2:

import { metodo1, metodo2, metodo3 } from './utils.js'
jest.mock('./utils.js', () => ({
  ...jest.requireActual('./utils.js'),
  metodo3: jest.fn().mockImplementation(() => 'Se ha llamdao al metodo3 simulado'),
}))
describe('named exports - archivo parcialmente simulado', () => {
  it('devuelve el mensaje correcto para método 1', () => {
    expect(method1()).toBe('Este es el método 1')
  })
  it('devuelve el mensaje correcto para método 2', () => {
    expect(method2()).toBe('Este es el método 2')
  })
  it('devuelve el mensaje correcto para método 3', () => {
    expect(method3()).toBe('Se ha llamdao al metodo3 simulado')
  })
})

Simulando exports default

¡Hemos cubierto bastantes casos de uso para la simulación de módulos! Pero, cada uno de los escenarios que hemos considerado hasta ahora utiliza exportaciones con nombre. ¿Cómo simulamos nuestro módulo si en su lugar hiciera uso de una exportación predeterminada?


Ahora imaginemos que nuestro archivo utils.js solo tiene un método que se exporta como su exportación predeterminada de esta manera:

const metodo1 = () => 'Usted invoco al método 1'
export default metodo1

Para probar este método sin simularlo, escribiríamos una prueba como esta:

import metodo1 from './utils.js'
describe('default export - sin simular', () => {
  it('Regresa el valor correcto para metodo 1', () => {
    expect(metodo1()).toBe('Usted invoco al método 1')
  })
})

Si quisiéramos simular automáticamente el módulo, podríamos usar el método jest.mock nuevamente; al igual que hicimos con nuestro módulo que usaba exportaciones con nombre:

import metodo1 from './utils.js'
jest.mock('./utils.js')
describe('default export - simulacion automatica', () => {
  it('Regresa el valor correcto para metodo 1', () => {
    expect(metodo1()).not.toBe('Usted invoco al método 1')
    expect(metodo1()).toBe(undefined)
  })
})

Si necesitamos más control sobre cómo se ve la función simulada, podemos usar nuevamente el método de fábrica de módulos. Sin embargo, aquí es donde las cosas difieren de nuestro enfoque anterior con exportaciones con nombre.

Para simular con éxito un módulo con una exportación predeterminada, necesitamos devolver un objeto que contenga una propiedad para __esModule: true y luego una propiedad para la exportación predeterminada. Esto ayuda a Jest a simular correctamente un módulo ES6 que utiliza una exportación predeterminada.

import metodo1 from './utils.js'
jest.mock('./utils.js', () => ({
  __esModule: true,
  default: () => 'Usted invoco al método 1 simulado',
}))
describe('default export - simulado con metodo factory', () => {
  it('Regresa el valor correcto para metodo 1', () => {
    expect(metodo1()).toBe('Usted invoco al método 1 simulado')
    expect(() => expect(metodo1).toHaveBeenCalledTimes(1)).toThrow()
  })
})

Si necesitamos poder espiar nuestro método, podemos usar el método mockImplementation que hemos usado antes. Tenga en cuenta que esta vez no tenemos que usar el indicador __esModule: true:

import metodo1 from './utils.js'
jest.mock('./utils.js', () => jest.fn().mockImplementation(() => 'Usted invoco al método 1 simulado'))
describe('default export - module factory with mock implementation mocked file', () => {
  it('Regresa el valor correcto para metodo 1', () => {
    expect(metodo1()).toBe('Usted invoco al método 1 simulado')
    expect(metodo1).toHaveBeenCalledTimes(1)
  })
})

Para un módulo que solo tiene una única exportación que es la exportación predeterminada, no tendremos ninguna forma de simular solo parcialmente el módulo (por lo que ese caso no es aplicable aquí).

Categorized in:

Tagged in:

, , ,