Técnicas de Mocking en tests de Frontend
En cualquier desarrollo Frontend es común interactuar con fuentes externas que proveen datos o realizan acciones. Este escenario añade algunas dificultades a la hora de desarrollar y escribir tests, puesto que no solo debes preocuparte por la lógica de tu componente, sino que también debes preocuparte de cómo este se comporta según el contexto en el que está y los datos que recibe.
En este artículo veremos las diferentes técnicas que puedes usar para afrontar los retos que te plantea este escenario.
Aclaraciones sobre la terminología
A lo largo del artículo, cuando se menciona la palabra "mocks", hace referencia a cualquier tipo de test double (dummy, stub, fake, spy o mock), no al concepto específico de mock como tal.
De la misma forma, se menciona el concepto API (Application Programming Interface), en este caso se refiere a la interfaz de una aplicación, independientemente de la tecnología o formato. Por ejemplo, una API de una aplicación o servicio puede ser una API REST, un SDK de una librería, o cualquier otro mecanismo de comunicación entre servicios.
Los motivos para necesitar Mocks
Entender los motivos por los que necesitas usar mocks es fundamental para poder usarlos de manera efectiva.
- Testing: Cuando desarrollas una funcionalidad y necesitas cubrir con tests todos los casos de uso.
- Desarrollo en paralelo: Cuando desarrollas una funcionalidad que depende de datos externos.
- Romper dependencias: Para trabajar sin depender de un servicio externo, evitando cuellos de botella.
En todos esos casos, el uso de mocks permite simular cualquier casuística, ya sea de comportamiento de una API, servicio externo o de otro tipo que veremos a continuación.
Sin embargo, antes de entrar en detalle, quiero matizar que el uso de mocks no es una solución a todos los problemas cuando estamos haciendo tests. Dado que el enfoque más habitual es el de crear tests de integración, es importante tratar de testear la funcionalidad completa, dejando el uso de mocks para los casos puntuales en los que no tenemos alternativa.
Técnicas de Mocking
En Frontend, existen diferentes aspectos que nos llevan a necesitar usar mocks. A continuación te detallo los casos más habituales, el porqué y las técnicas que puedes usar para afrontarlos.
Si consideras que me he dejado algún caso importante, no dudes en contactar conmigo.
API de servicios externos
Considero que un servicio API es cualquier servicio que utiliza un mecanismo de comunicación con la aplicación para obtener datos o realizar acciones. El mecanismo de comunicación más común es el uso de HTTP (REST, SOAP, GraphQL, etc), pero no es el único.
Teniendo esto claro, cualquier aplicación Frontend que consuma datos de un servicio API externo va a necesitar mocks para poder testear la funcionalidad de la aplicación y cubrir todos los casos de uso.
Cómo hacer mocks de API de servicios externos
Para mockear una API de servicios externos, existen diferentes técnicas que puedes utilizar. Las más conocidas son el tradicional mock server y el uso de librerías como MSW.
Siempre recomiendo el uso de MSW, que es una librería que te permite interceptar las peticiones HTTP y simular respuestas, por los siguientes motivos:
Simplicidad: No necesitas crear ni mantener un servidor HTTP para mockear la API. No necesitas apuntar las requests a la URL específica del mock server y tampoco debes preocuparte de arrancar un servidor antes de arrancar la aplicación o ejecutar los tests. Además, permite ejecutarse en servidor y en el navegador.
Flexibilidad: Permite simular cualquier tipo de respuesta HTTP, incluyendo errores, tiempo de respuesta, etc.
Integración: Se integra perfectamente con cualquier framework de testing.
Aquí tienes un ejemplo básico de un test para React con Jest:
const getUserHandler = http.get(
"https://jsonplaceholder.typicode.com/users/:userId",
({ request }) => {
const authorization = request.headers.get('Authorization')
if (authorization !== 'Bearer 1234567890') {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return HttpResponse.json({ id: 1, name: "John Doe", username: "jd" })
}
)
describe('Given a UserCard component', () => {
describe('When the user is logged in', () => {
test('Then it should display the user name', async () => {
server.use(getUserHandler)
render(<UserCard id="1" />)
expect(screen.getByRole('heading', { name: 'John Doe' })).toBeVisible()
})
})
})
SDK o librerías de terceros
Un SDK o una librería es cualquier conjunto de utilidades que proporciona una interfaz programática para interactuar con un servicio.
En este caso particular, el motivo de usar mocks no debe ser simular el comportamiento, sino evitar la interacción con el servicio que hay detrás. Aunque no es habitual, es posible que para algún caso de uso específico sí sea necesario mockear el comportamiento.
Cómo hacer mocks de SDK o librerías de terceros
Existen infinidad de motivos por los que necesites mockear el comportamiento de una librería o SDK de terceros.
Aquí tienes un ejemplo concreto de un test para React con Vitest, en el que mockeamos el comportamiento de la librería
react-chartjs-2
para poder testear el componente BarChart
de manera independiente.
De esta forma evitamos la interacción del componente de la librería, evitando problemas con la interactividad de canvas
en el entorno de ejecución del test.
vi.mock('react-chartjs-2', () => ({
Bar: (props: any) => <div {...props}></div>,
}))
describe('Given a BarChart component', () => {
describe('When the component is rendered', () => {
test('Then it should present the chart.', () => {
render(<BarChart />)
expect(screen.getByRole('presentation')).toBeVisible()
})
})
})
Simulación de callbacks
En ocasiones, algún componente o función ejecuta un callback que recibe como parámetro. Es habitual que para estos casos concretos, utilicemos mocks por varios motivos:
- Para verificar que el callback se ejecuta con los parámetros esperados.
- Para simular el comportamiento del callback.
- Para verificar efectos secundarios del callback.
Cómo hacer mocks de callbacks
En este caso, será tan sencillo como crear un spy para el callback y pasarlo por prop. Luego, podremos verificar que el callback se ejecuta con los parámetros esperados, tras simular el comportamiento del usuario que deriva en la ejecución del callback.
describe('Given a Button component', () => {
describe('When the button is clicked', () => {
test('Then it should call the onClick function', async () => {
const onClick = vi.fn()
render(<Button onClick={onClick} />)
await userEvent.click(screen.getByRole('button'))
expect(onClick).toHaveBeenCalled()
})
})
})
Contextualización temporal
Toda aplicación Frontend se puede comportar de manera diferente según el momento y el contexto en el que se encuentra.
Algunos casos concretos derivados de una aplicación que tiene una URL específica y se carga en un navegador concreto pueden ser:
- La aplicación tiene un estado de navegación concreto en la herramienta de routing utilizada.
- La aplicación ofrece los datos de autenticación del usuario disponibles desde cualquier componente.
- La aplicación aplica un idioma y formato de fechas específico.
- La aplicación se dispone a cargar unos datos concretos.
- La aplicación muestra un mensaje de error.
Cómo hacer mocks de contextualización temporal
En el siguiente test se crea un mock en el contexto que almacena la información del usuario para poder testear cómo se comporta el componente tras proporcionarle un valor simulado al contexto. Este tipo de mocks nos permite simular todos los estados posibles y cubrir todos los casos de uso con tests.
describe('Given a UserProfile component', () => {
describe('When the user is logged in', () => {
test('Then it should display the user information', () => {
const mockUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com'
};
render(
<UserProvider user={mockUser}>
<UserProfile />
</UserProvider>
);
expect(screen.getByRole('heading', { name: 'John Doe' })).toBeVisible();
expect(screen.getByText('john@example.com')).toBeVisible();
});
});
});
Es habitual encontrar una abstracción del método render
en los tests de Frontend, que nos permite
renderizar el componente con todos los contextos que tenga la aplicación.
const testQueryClient = new QueryClient(/* ... */)
type ProvidersProps = {
user: User
}
const Providers = ({ children, user }: PropsWithChildren<ProvidersProps>) => {
return (
<QueryClientProvider client={testQueryClient}>
<UserContext.Provider user={user}>{children}</UserContext.Provider>
</QueryClientProvider>
)
}
export const renderWithProviders = (
ui: React.ReactElement,
options?: RenderOptions,
providersProps?: ProvidersProps
) =>
render(ui, {
wrapper: (props: PropsWithChildren) => (
<Providers {...props} user={providersProps?.user} />
),
...options,
})
Control del tiempo en operaciones asíncronas
Al utilizar setTimeout
, setInterval
o debounce
, entre otros, los mocks nos permiten controlar el tiempo de
ejecución. Esto puede ayudar a acelerar los tests, pero cuidado: esto puede provocar problemas de asincronía si no se revisa bien la implementación.
Cómo hacer mocks de control del tiempo en operaciones asíncronas
Tanto Jest como Vitest proporcionan useFakeTimers
, una forma de mockear el tiempo de ejecución permitiendo
controlar el tiempo de ejecución de las operaciones asíncronas.
En este caso te recomiendo ir a la documentación oficial de Jest o Vitest para ver cómo se utiliza.
Recursos de interés
- Watch "Mocking techniques in Vitest" - Artem Zakharchenko, EpicWeb
- Watch "Testing JavaScript" - Kent C. Dodds
- Read Vitest mocking documentation
- Read Jest mock documentation
- Read Mock Service Worker documentation for API mocking
- Read "La guía definitiva de los dobles de test" parte 1 - Fran Iglesias
- Read "La guía definitiva de los dobles de test" parte 2 - Fran Iglesias