Testing Custom Hooks

Testing Custom Hooks

¿Cómo podemos testear los custom hooks de nuestras aplicaciones?

Vamos a ver los pasos que tenemos que dar para poder testear los custom hooks que podemos ir desarrollando en nuestras aplicaciones de React y para ello vamos a partir del siguiente custom hook:

import { useState } from 'react'

type UseCounterArgs = {
  initialCount?: number
}

export const useCounter = ({ initialCount = 0 }: UseCounterArgs = {}) => {
  const [count, setCount] = useState(initialCount)

  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)

  return {
    count,
    increment,
    decrement
  }
}

Como podemos ver el nuestro custom hook es realmente simple puesto que lo único que hacemos dentro del mismo es definir un atributo del estado count al que se le asignará un valor por defecto (que podrá haber sido pasado como parámetro) y se implmentarán las dos funciones que nos van a permitir incrementar o decrementar su valor.

Test

Vamos a crear los test que nos permitirán probar el correcto funcionamiento de useCounter. Para ello lo primero que tenemos que hacer es importar la función render de react testing library:

import { render } from '@testing-library/react'

Ahora importaremos nuestro custom hook pensando que no es más que la función que vamos a querer probar:

import { render } from '@testing-library/react'
import { useCounter } from './useCounter'

Ahora podemos pasar a utilizar la función describe para definir la suite de test que estará asociada a todos los test que tienen que ver con nuestro custom hook:

describe('useCounter', () => {})

Con esto ya tenemos definida nuestra suite ya podemos pasar a escribir cada uno de los test.

Testear el valor inicial

El primero de los test que vamos a implementar tendrá que ver con probar que el valor inicial por defecto (es decir, que no se le pasa ningún parámetro a nuestro custom hook) es cero. Así escribiremos:

it('should render the initial count', () => {})

¿Qué pasos vamos a tener que dar dentro de nuestro test? Pues en primer lugar tendremos que llamar a la función render pero en este caso le pasaremos como parámetro el hook que estamos probando:

it('should render the initial count', () => {
  render(useCounter)
})

Si embargo, si escribimos este código dentro de nuestro editor podemos ver cómo TypeScript nos estará informando de un error en un tooltip:

¿Por qué se está produciendo este error? Pues esto es debido a que la función render lo que acaba retornando como resultado de su ejecución es código JSX (es decir, un ReactElement) y un custom hook no retorna este tipo de datos.

Por lo tanto parece razonable pensar que un custom hook no va a poder ser llamado fuera de un componente de React pero esto vendría a complicar la lógica de nuestros test puesto que al final lo que tendríamos que hacer sería crear un componente de React que sería únicamente utilizado para los test y por lo tanto introducido de una forma artificial en nuestro código.

¿Y por qué no invocamos directamente a la función useCounter dentro del código? Es decir, ¿por qué no usamos algo como lo siguiente?

it('should render the initial count', () => {
  useCounter()
})

El código, desde el punto de vista de TypeScript, es correcto pero el problema lo tenemos en el momento en el que ejecutaremos nuestros test puesto que nos aparecerá un error parecido al que se puede ver en la siguiente imagen:

Este error básicamente nos viene a decir que estamos haciendo una llamada a un custom hook de forma directa y esto no es posible puesto que los hooks solamente pueden ser invocados dentro de un componente de React.

Entonces ¿cómo vamos a poder probar nuestros hooks? La respuesta está una vez más en la react testing library puesto que nos proporciona la función renderHook para este propósito por lo que esta será la función que tendremos que importar en vez de render:

import { renderHook } from '@testing-library/react'

Ahora tenemos que pasar a usar esta función en nuestro hook sabiendo que como parámetro lo que espera recibir es el custom hook que vamos a probar. Así pues escribiremos:

it('should render the initial count', () => {
  renderHook(useCounter)
})

Pero ¿cómo podemos hacer ahora nuestras aserciones? En el caso de un estar testeando un componente de React ya hemos visto que es gracias al uso del objeto screen que nos proporciona react testing library en el caso de los custom hook no se va a poder hacer uso de este objeto. Aquí es donde tenemos que saber que react testing library internamente lo que está haciendo cuando invocamos a renderHook es envolver el hook que recibe como parámetro en un funcional componente y retornará el resultado de la invocación en un objeto donde en su atributo result tendremos el resultado de la invocación del custom hook.

it('should render the initial count', () => {
  const { result } = renderHook(useCounter)
})

Tenemos además que saber que dentro del objeto result tenemos a nuestra disposición el atributo current que lo que viene a tener son todos los valores que han sido retornados por el custom hook que estamos probando.

Con esto en mente ya podemos crear nuestra aserción puesto que uno de los atributos de current en nuestro ejemplo, será count y por lo tanto podemos escribir algo como:

it('should render the initial count', () => {
  const { result } = renderHook(useCounter)
  expect(result.current.count).toBe(0)
})

Si ahora guardamos nuestro trabajo y ejecutamos el test podemos ver que este pasará sin problemas tal y como esperábamos:

De hecho si cambiamos nuestra aserción intentado asegurar que el valor que se recibe es diferene de cero:

it('should render the initial count', () => {
  const { result } = renderHook(useCounter)
  expect(result.current.count).toBe(1)
})

podemos ver como en la consola nos aparecerá un error tal y como esperábamos:

Test valor inicial cuando se pasa como parámetro

Vamos con el segundo de nuestros test en el cual vamos a probar que nuestro custom hook aceptará el valor inicial que ha de tener el contador. Así pues definimos:

it('should accept and render the same initial count', () => {})

Antes hemos visto que para poder llamar a nuestro custom hook lo que vamos a usar es la función renderHook la cual espera recibir como parámetro el custom hook que vamos a probar useCounter pero ¿cómo podemos pasarle a este custom hook los parámetros con el que queremos invocarlo? La respuesta es sabiendo que renderHook permite tener un segundo parámetro que será un objeto con las opciones que queremos que se apliquen.

De entre todos los atributos que pueden venir definidos en este objeto de opciones tenemos a nuestra disposición initialProps a la que le vamos a poder asignar un objeto donde definiremos un atributo por cada uno de los parámetros que espera recibir nuestro custom hook y el valor que tiene asignado. Así, si quieremos que en nuestro test se pruebe que el valor inicial es 10 escribiríamos algo como:

it('should accept and render the same initial count', () => {
  renderHook(useCount, {
    initialProps: {
      initialCount: 10
    }
  })
})

Hemos definido un objeto como el valor de initialProps como un objeto puesto que es lo que espera recibir.

Ahora el proceso de definición de nuestro test será igual que en el caso anterior puesto que lo que haremos será quedarnos con el atributo result como sigue donde además escribiremos la aserción para comprobar que el valor que tendrá el contador será 10:

it('should accept and render the same initial count', () => {
  const { result } = renderHook(useCount, {
    initialProps: {
      initialCount: 10
    }
  })

  expect(result.current.count).toBe(10)
})

Si ahora guardamos nuestro los cambios y volvemos a ejecutar los test podrems ver que los dos pasarán correctamente tal y como esperábamos:

El código completo del test que hemos escrito lo podemos encontrar a continuación:

import { renderHook } from '@testing-library/react'
import { useCounter } from './useCounter'

describe('useCounter', () => {
  it('should render the initial count', () => {
    const { result } = renderHook(useCounter)
    expect(result.current.count).toBe(0)
  })

  it('should accept and render the same initial count', () => {
    const { result } = renderHook(useCount, {
      initialProps: {
        initialCount: 10
      }
    })

    expect(result.current.count).toBe(10)
  })
})

Resumen

  • Cuando vamos a testear nuestros custom hooks tenemos que apoyarnos en el uso de la función renderHook que nos proporciona react testing library. Esta función espera recibir como parámetro el custom hook que vamos a probar.

  • La invocación de renderHook retornará un objeto que posee el atributo result que tiene a su vez el atributo current cuyo valor será un objeto que contiene como atributos todos los valores que retorna el custom hook que ha recibido como parámetro en su invocación.

  • En el caso de tener que pasar parámetros al custom hook lo podremos hacer gracias al segundo parámetro que puede recibir renderHook que no es más que un objeto con las opciones que queremos que aplique. En este caso haríamos uso del atributo initialProps.

Nota: dejamos para un próximo artículo el ver cómo podemos probar las funciones increment y decrement de nuestro custom hook de ejemplo.