Fecha de publicación

Mockeando hooks de Apollo en React

Autores

ImBackBbs

Pues bueno, parece que ha pasado ya más de un año desde mi última publicación (con una pandemia de por medio y mil catástrofes que parecía que se iba a acabar el mundo, o en su defecto el papel higiénico en los supermercados :roll_of_paper:).

Así que nada antes de empezar me gustaría felicitar a todos los lectores unas felices y seguras fiestas y que Papa Noel os haya traído muchas mascarillas! :santa: :mask:

Manos a la obra!

Una de las adversidades que he ido viendo en los proyectos que he estado trabajando y en la gente que lo forma, es la oscuridad que hay detrás de los mocks (en este caso de jest) y el cómo su uso nos puede beneficiar tanto en tiempo de desarrollo como a la hora de escribir nuestras pruebas unitarias.

Un ejemplo que a primera vista parece MUY sencillo, luego a la hora de hacer sus pruebas unitarias puede llegar a ser enrevesado, el caso es el siguiente:

Tenemos un component que queremos transformar sus props a un objeto mas "component friendly", para así luego renderizar nuestro html con dichas props ya mappeadas:

dummy hook que nos facilita la vida (nos olvidamos del ApolloMockProvider y movidas varias)

import * as ApolloReactHooks from '@apollo/react-hooks'
import gql from 'graphql-tag'

const QUERY_USER = gql`
  query getUser {
    user {
      data {
        firstName
        lastName
        age
      }
    }
  }
`

export function useGetUserLazyQuery(baseOptions: ApolloReactHooks.QueryHookOptions) {
  return ApolloReactHooks.useLazyQuery(QUERY_USER, baseOptions)
}

sí, lo sé, no he importado React, beneficios de usar React 17 :grimacing:

fake component que efectúa la llamada a nuestro backend y nos muestra por pantalla la información del usuario una vez recibida la query

import { useState } from 'react'
import { useGetUserLazyQuery } from '@hooks/useGetUserLazyQuery'

interface User {
  name: string
  age: number
}

export function User() {
  const [user, setUser] = useState<Partial<User>>({})
  const [queryUser, { called, loading }] = useGetUserLazyQuery({
    onCompleted: ({ data: { firstName, lastName, age } }) => {
      const user: User = {
        name: `${firstName} ${lastName}`,
        age: age,
      }
      setUser(user)
    },
  })

  const performQuery = () => {
    queryUser()
  }

  if (called && loading) return <p>Loading ...</p>

  if (!called)
    return (
      <button data-testid="load-users-btn" onClick={performQuery}>
        Load user
      </button>
    )

  return (
    <div>
      <ul>
        <li>Name: {user.name}</li>
        <li>Age: {user.age}</li>
      </ul>
    </div>
  )
}

Ahora viene dónde la matan, como podemos invocar el callback onCompleted en nuestro caso? :thinking:

Que no cunda el pánico, no vamos a tener que hacer refactor de nuestro componente para que use un useEffect para reaccionar al cambio de la prop data ni nada por el estilo para que nuestro test pase :white_check_mark:

import { screen, render, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useGetUserLazyQuery } from '@hooks/useGetUserLazyQuery'
import { User } from '../User'

jest.mock('@hooks/useGetUserLazyQuery', () => ({
  useGetUserLazyQuery: jest.fn(),
}))

const userMock = {
  data: {
    firstName: 'Freddy',
    lastName: 'Krueger',
    age: 288,
  },
}

function renderComponent() {
  return render(<User />)
}

function mockQuery({ result }) {
  let onCompletedHasBeenCalled = false

  const getUserCall = jest.fn()

  ;(useGetUserLazyQuery as jest.Mock).mockImplementation((d) => {
    // este condicional previene que la llamada a OnCompleted acabe en un bucle infinito y nos fastidie todo, únicamente lo ejecutaremos una única vez
    if (!onCompletedHasBeenCalled) {
      d.onCompleted(result)
      onCompletedHasBeenCalled = true
    }

    return [getUserCall, { called: true, loading: false }]
  })

  return getUserCall
}

describe('User component', () => {
  it('renders user data', async () => {
    const mockedQuery = mockQuery({ result: userMock })
    renderComponent()

    userEvent.click(await screen.findByTestId('load-users-btn'))

    const {
      data: { age, firstName, lastName },
    } = userMock

    expect(screen.getByText(age)).toBeInTheDocument()
    expect(screen.getByText(firstName, { exact: false })).toBeInTheDocument()
    expect(screen.getByText(lastName, { exact: false })).toBeInTheDocument()

    await waitFor(() => {
      expect(mockedQuery).toHaveBeenCalled()
    })
  })
})

Toda la magia recae en el mockImplementation, donde vamos a poder acceder a los argumentos que nuestro custom hook está recibiendo, de esta manera nos evitamos tener que mockear TODA la llamada que esperaría el provider de apollo, así pues, evitamos tener que renderizar componentes externos que para nuestra prueba unitaria no tiene mucho sentido, y nos podemos centrar exclusivamente en la funcionalidad de dicho componente.

Lo bueno si breve, dos veces bueno y aun lo malo, si poco, no tan malo

Como diría el gran don Hector del Mar: "Aquí estoy porque he venido, porque he venido aquí estoy, si no te gusta mi canto como he venido me voy"

Que tengáis una feliz entrada de 2021 donde los mocks de apollo no sean un problema!!!

CYa!

Nos vamos... pero volveremos!