Durante los últimos años se han introducido cambios significativos en el ecosistema de React, como la llegada de los hooks o la adopción de React Testing Library como nuevo standard para tests. Esto hace necesario nuevos métodos para testear las aplicaciones y conseguir una buena cobertura.

Para ejemplificarlo, vamos a basarnos en un proyecto sencillo de chat (repositorio en Github) que, a pesar de ser pequeño, cubre una buena cantidad de casos. Por supuesto, una lectura completa de la documentación de Jest y React Testing Library es muy recomendable también.

Chat con React Testing Library

Pre-requisitos

Proyecto creado con create-react-app o con Jest y React Testing-library y convenientemente configurado para ejecutar tests.

npm install --dev @testing-library/jest-dom @testing-library/react @testing-library/react-hooks @testing-library/user-event

@testing-library/user-event no viene instalada por defecto con proyectos create-react-app

Tests unitarios para componentes

React testing library introduce una nueva API cuyos principales métodos son render y screen. Con render podemos montar un componente de la forma habitual en React y con el singleton screen podemos leer lo que hay en él.

Componentes sin funcionalidad

Empezando por el ejemplo más básico (aunque necesario para conseguir cobertura) para un componente sin funcionalidad podemos probar que se renderiza bien y que tiene alguna característica determinada.

// src/components/Logo.js

export default () => (
 <h1>
   CHAT
 </h1>
)

El componente para el logotipo de la aplicación tiene solo eso: un texto con la palabra “chat” y ninguna funcionalidad. Podemos probarlo de una manera muy sencilla así:

// src/components/Logo.test.js

import { render, screen } from '@testing-library/react'
import { Logo } from 'components'

describe('Logo', () => {
 it('renders appropriately', () => {
   render(<Logo />)
   expect(screen.getByText(/chat/i)).toBeInTheDocument()
 })
})

Lo que hacemos en dos pasos es primero renderizar aisladamente el componente Logo, y segundo buscar en screen las características que lo definen, como el que haya un texto que coincida con “CHAT”. No hace falta guardar en una constante el contenido de render porque screen siempre tiene disponible lo último que se haya renderizado.

Hasta este momento solo necesitamos los selectores tipo “get” de React testing Library. Son getByRole, getByLabelText, getByPlaceholderText, getByText o getByDisplayValue y lo que hacen es buscar en el contenedor dado, un elemento que tenga las características de texto pasadas como parámetro con un string o con un regexp. Podemos leer más acerca de los selectores de React Testing Library aquí.

Componentes que usan hooks personalizados

La aplicación chat-room utiliza react-query en forma de custom hooks, que es la manera recomendada para mejorar la legibilidad y facilitar los tests.

Un caso ligeramente más complicado que el que acabamos de ver es este de un componente que lee datos obtenidos de un hook useParticipants, que únicamente le devuelve una lista de participantes activos en ese momento. El contenido de este hook lo veremos en un momento pero por el momento, centrémonos en probar el componente que lo usa.

// src/components/participants.js
import { ParticipantCard } from 'components'
import { useParticipants } from 'hooks'

export default () => {
 const participants = useParticipants()

 return (
   <section
     data-testid="participants"
   >
     {participants.map(participant => (
       <ParticipantCard
         key={participant.id}
         {...participant}
         data-testid="participants-participant-card"
       />
     ))}
   </section>
 )
}

Que podemos probar así:

// src/components/participants.test.js
import { render, screen } from '@testing-library/react'
import { Participants } from 'components'
import { useParticipants } from 'hooks'

jest.mock('../hooks/useParticipants')

describe('Participants', () => {
 beforeEach(() => {
   const participants = [
     { id: 0, name: 'Andrea' },
     { id: 1, name: 'Pablo' },
     { id: 2, name: 'Juanma' },
   ]
   useParticipants.mockImplementation(() => participants)
 })

 it('has same amount of cards as participants are provided', () => {
   render(<Participants />)
   expect(screen.getAllByTestId('participant-card')).toHaveLength(3)
 })
})

Recordemos que estos son tests unitarios así que probamos que el componente funciona mockeando todas sus dependencias. En este caso, vemos que mockeamos el hook useParticipants con jest.mock(‘../hooks/useParticipants’) y en un paso posterior mockeamos su implementación para que devuelva siempre unos datos conocidos: useParticipants.mockImplementation(() => participants). De esta manera, luego podemos asertar con un selector que la lista de participantes será de longitud 3, ya que es lo que le hemos pasado inicialmente. Así estamos probando el listado de participantes y no su dependencia.

Notar también que en lugar de seleccionar por el texto que contiene el elemento estamos usando los selectores getByTestId y getAllByTestId, que buscan el componente html cuyo atributo data-testid coincida con el previsto.

Componentes con interactividad

Algo más de imaginación y meticulosidad requiere probar componentes con interactividad que permitan al usuario hacer click, escribir, etc… Para estos casos React Testing Library expone una api como userEvent que va a cubrir todas nuestras necesidades.

El componente MessageInput se compone de un input de texto y un botón, que al pulsarlo envía el texto a un servicio que lo publica en el chat.

// src/components/MessageInput.js
import { useRef } from 'react'
import { FormInput, FormButton } from 'components'
import { useCurrentUser, useMutatePostMessage } from 'hooks'

export default () => {
 const input = useRef()
 const button = useRef()
 const user = useCurrentUser()
 const mutate = useMutatePostMessage()

 return (
   <section
     data-testid="test-input"
   >
     <FormInput>
       <input
         data-testid="message-input-text"
         disabled={!user}
         onKeyUp={({ key }) => key === 'Enter' && button.current.click()}
         ref={input}
       />
     </FormInput>
     <FormButton
       data-testid="message-input-button"
       disabled={!user}
       onClick={() => {
         const content = input.current.value
         if (content === '') return
         mutate({ user, content })
         input.current.value = ''
       }}
       ref={button}
     >
       SEND
     </FormButton>
   </section>
 )
}

De entrada vemos que el componente depende de dos hooks que son useCurrentUser y useMutatePostMessage, así que tendremos que mockearlos en el test. Además, vemos que hay diferentes casos contemplados, como que el usuario no esté logado, que se intente enviar un mensaje sin texto o que se use la tecla intro en lugar del botón. Si queremos una buena cobertura, tendremos que contemplar todos estos casos uno a uno.

// src/components/MessageInput.test.js
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MessageInput } from 'components'
import { useCurrentUser, useMutatePostMessage } from 'hooks'

jest.mock('../hooks/useCurrentUser')
jest.mock('../hooks/useMutatePostMessage')

describe('MessageInput', () => {
// Un punto básico de entrada puede ser comprobar si se presentan todos los componentes esperados. En este caso usamos getByRole, que los encuentra de acuerdo a su rol aria.
 it('renders properly', () => {
   render(<MessageInput />)
   expect(screen.getByRole('textbox')).toBeInTheDocument()
   expect(screen.getByRole('button')).toBeInTheDocument()
 })

 // El componente tiene un comportamiento diferente dependiendo de si el usuario está logueado o no, así que podemos describirlo de las dos maneras, simular el estado y comprobar.
 describe('user is not logged in', () => {
   beforeEach(() => {
     // De esta manera simulamos que el usuario no está logueado presentemente.
     useCurrentUser.mockImplementation(() => undefined)
   })

   it('is disabled', () => {
     render(<MessageInput />)
     expect(screen.getByRole('textbox')).toBeDisabled()
     expect(screen.getByRole('button')).toBeDisabled()
   })
 })

 describe('user is logged in', () => {
   const user = { id: 1, name: 'Alberto' }
   beforeEach(() => {
     // Moqueando useCurrentUser con un usuario inventado simulamos que el usuario está logueado.
     useCurrentUser.mockImplementation(() => user)
   })

   it('is enabled', () => {
     render(<MessageInput />)
     expect(screen.getByRole('textbox')).toBeEnabled()
     expect(screen.getByRole('button')).toBeEnabled()
   })

   // Comprobando que determinado evento ocurra como resultado de una interacción del usuario.
   describe('post button is clicked', () => {
     const mutate = jest.fn()

     beforeEach(() => {
       // Si comprobamos nuestro código veremos que useMutatePostMessage devuelve la función que se ejecuta cuando se envía un mensaje. Podemos mockearla así y ver si se ha ejecutado al realizar la acción.
       useMutatePostMessage.mockImplementation(() => mutate)
     })

     describe('nothing is written in', () => {
       it('does not post message', () => {
         render(<MessageInput />)
         userEvent.click(screen.getByRole('button'))
         expect(mutate).not.toHaveBeenCalled()
       })
     })

     describe('something is written in', () => {
       const content = 'something written in'
       it('posts message', () => {
         render(<MessageInput />)
         userEvent.type(screen.getByRole('textbox'), content)
         userEvent.click(screen.getByRole('button'))
         expect(mutate).toHaveBeenCalledWith({ user, content })
       })
     })
   })

   describe('ENTER key is pressed', () => {
     const mutate = jest.fn()

     beforeEach(() => {
       useMutatePostMessage.mockImplementation(() => mutate)
     })

     describe('nothing is written in', () => {
       it('does not post message', async () => {
         render(<MessageInput />)
         await userEvent.type(screen.getByRole('textbox'), '{enter}')
         expect(mutate).not.toHaveBeenCalled()
       })
     })

     describe('something is written in', () => {
       const content = 'something written in'

       it('posts message', () => {
         render(<MessageInput />)
         userEvent.type(screen.getByRole('textbox'), content)
         userEvent.type(screen.getByRole('textbox'), '{enter}')
         expect(mutate).toHaveBeenCalledWith({ user, content })
       })
     })
   })
 })
})

Lo más novedoso en esta parte seguramente es la técnica para mockear una función y después comprobar que ha sido llamada tras un evento y el uso de la librería userEvent, que nos proporciona una manera realista de simular eventos como los que provocaría un usuario, como hacer click en un elemento, hacer hover, rellenar un campo de un formulario tecla a tecla, etc… Disponemos también de la función fireEvent, pero actualmente está recomendado usar userEvent por ser la más realista de las dos.

Tests unitarios para hooks

Por supuesto que las pruebas para componentes son una habilidad compleja y extensa pero hemos cubierto bastante terreno con estas nuevas técnicas y APIs y seguramente nos dejen cercanos al 100% de cobertura siempre que nos organicemos bien y tengamos el código bien abstraído. Como decíamos al principio, lo recomendable es tener los hooks aislados como custom hooks para simplificar su mockeado en los componentes y para probarlos por separado como vamos a hacer ahora.

En este caso concreto, hemos abstraído el hook de react-query que hace la petición para obtener las llamadas que nos proporciona el servicio de servidor.

// src/hooks/useMessajes.js
import { useQuery } from 'react-query'
import { getMessages } from 'api'

// Long polling for messages.
export default () => {
 const { data = [] } = useQuery('messages', async () => await getMessages(), {
   keepPreviousData: true,
   refetchInterval: 500,
 })
 return data
}

Un hook muy sencillo que se resuelve en una sola sentencia, pero probar hooks, que por norma no pueden ejecutarse fuera de los componentes, es un reto y vamos a necesitar una librería auxiliar para conseguirlo, react-hooks, que ya instalamos en los primeros pasos de este tutorial.

// src/hooks/useMessages.test.js
import { renderHook } from '@testing-library/react-hooks'
import { getMessages } from 'api'
import { useMessages } from 'hooks'

jest.mock('../api/getMessages')

const messages = [
 { id: 0, user: { id: 0, name: 'Alberto' }, content: 'xxxx' },
 { id: 1, user: { id: 0, name: 'Alberto' }, content: 'yyyy' },
 { id: 2, user: { id: 1, name: 'Ana' }, content: 'zzzz' },
]

describe('useMessages', () => {
 describe('with each update', () => {
   it('returns more messages', async () => {
     getMessages.mockImplementation(() => Promise.resolve(messages))

     const { result, waitForNextUpdate } = renderHook(() => useMessages())
     await waitForNextUpdate()
     expect(result.current).toHaveLength(3)
     getMessages.mockImplementation(() =>
       Promise.resolve([...messages, ...messages]),
     )
     await waitForNextUpdate()
     expect(result.current).toHaveLength(6)
   })
 })
})

Lo primero como de costumbre es moquear la dependencia, en este caso api/getMessajes.js, que probaremos aparte. Como getMessages es una promesa de fetch, la mockeamos acordemente: getMessages.mockImplementation(() => Promise.resolve(messages)). Puesto que vamos a trabajar con promesas, hacemos que el bloque entero sea async para poder esperar a que se resuelvan con await.
Importamos renderHook, que es la librería standard de facto para probar hooks aisladamente e instanciamos el hook en cuestión con la línea const { result } = renderHook(() => useMessages()), que nos devuelve varios posibles helpers y valores, el más importante de ellos result, que contiene el resultado real de ejecutar dicho hook.

Por último, como hemos controlado que se devuelvan tres mensajes, comprobamos que esto sea así y con eso damos por probado el hook.

En este caso particular se prueba a reimplementar con nuevos datos porque queremos comprobar que nuestro hook no pierde los iniciales, sino que se suman, pero esto es una característica propia de este hook y la manera en la que está implementado.

Tests para hooks de mutación

Son ligeramente diferentes a un hook de consulta. Por ejemplo, para el hook que hace un POST de un mensaje:

// src/hooks/useMutatePostMessage.js
import { useMutation } from 'react-query'
import { postMessage } from 'api'

export default () => {
 const [mutate] = useMutation(({ user, content }) =>
   postMessage({ user, content }),
 )
 return mutate
}

Lo probamos similarmente así:

// src/mocks/useMutatePostMessage.test.js
import { act, renderHook } from '@testing-library/react-hooks'
import { useMutatePostMessage } from 'hooks'
import { postMessage } from 'api'

jest.mock('../api/postMessage')

describe('useMutatePostMessage', () => {
 it('returns a function', () => {
   const { result } = renderHook(() => useMutatePostMessage())
   expect(result.current).toBeInstanceOf(Function)
 })

 describe('returned function', () => {
   it('posts a message', async () => {
     const user = { id: 1, name: 'Manolo' }
     const content = 'CONTENT'
     const { result, waitFor } = renderHook(() => useMutatePostMessage())
     await act(async () => {
       result.current({ user, content })
       await waitFor(() => result.current.isSuccess)
     })
     expect(postMessage).toHaveBeenCalledWith({ user, content })
   })
 })
})

Resumiendo, el hook devuelve una función que el componente puede utilizar para ordenar el post de un mensaje así que comprobamos que, efectivamente, el hook devuelve una función y que utilizarla llama a la función esperada api/postMessage.js y con los parámetros esperados.

Tests para llamadas a API con fetch

Partíamos de una aplicación con múltiples capas de abstracción y con esta llegamos finalmente a la parte donde realmente se hace la llamada.

// src/api/getParticipants.js
export default () => fetch('/participants').then(res => res.json())

Que probamos así:

// api/getParticipants.test.js
import { getParticipants } from 'api'

jest.spyOn(window, 'fetch')

const participants = [
 { id: 1, avatar: 'http://avatar.com/1', name: 'Virginia' },
 { id: 2, avatar: 'http://avatar.com/2', name: 'Mario' },
]

describe('getParticipants', () => {
 beforeAll(() =>
   window.fetch.mockImplementation(() => ({
     then: () => participants,
   })),
 )

 it('makes a fetch call', () => {
   getParticipants()
   expect(window.fetch).toHaveBeenCalledWith('/participants')
 })

 it('returns data', async () => {
   const data = await getParticipants()
   expect(data).toEqual(participants)
 })
})

Hemos tenido que mockear la función window.fetch y una de las posibles maneras es con jest.spyOn para luego sustituirla por una nueva implementación sobre la que llamar a su propiedad then y que nos devuelva el dato que nosotros controlamos.

Tests de integración

Lo que hemos visto hasta ahora eran tests unitarios, lo cual significa que, dado un determinado módulo de forma aislada, funciona de la forma esperada. Sin embargo, a un nivel superior, estas piezas separadas se encontrarán unidas de una forma organizada y la interacción entre ellas es lo que probamos con los tests de integración, que se colocan a un nivel de abstracción superior a los unitarios.

Una buena forma de distribuir los tests de integración es uno por página/pantalla/ruta si nuestra aplicación incluye varias de ellas. Como en nuestro caso solo hay una, podemos poner los tests adyacentes al componente principal App.js.

Nuestro App.js tiene solo unas pocas líneas para importar los componentes de la aplicación:

// src/App.js
import { ModalProvider } from 'react-modal-hook'
import {
 ChatView,
 Header,
 LoginController,
 MessageInput,
 Watermark,
} from 'components'

export default () => {
 return (
   <ModalProvider>
     <div className="absolute w-screen h-screen subpixel-antialiased bg-gray-600 justify-items-center">
       <Header />
       <LoginController />
       <ChatView />
       <MessageInput />
       <Watermark />
     </div>
   </ModalProvider>
 )
}

El archivo de prueba es mucho más largo porque estamos probando todas las interacciones entre usuario y componentes.

// src/App.test.js
import {
 act,
 render,
 screen,
 waitFor,
 waitForElementToBeRemoved,
 within,
} from '@testing-library/react'
import { userLogIn } from 'setupTests'
import { getMessages, getParticipants, logIn, postMessage } from 'api'
import App from './App'
import userEvent from '@testing-library/user-event'

jest.mock('api/logIn')
jest.mock('api/getParticipants')
jest.mock('api/getMessages')
jest.mock('api/postMessage')

const mockLoginError = () => {
 logIn.mockImplementationOnce(() => {
   throw new Error()
 })
}

const currentUser = {
 avatar: 'http://avatar.com/1',
 id: 1,
 name: 'rightuser',
}

const mockLoginSuccess = () => {
 logIn.mockImplementationOnce(() => Promise.resolve(currentUser))
}

describe('App', () => {
 it('has all of its components', () => {
   render(<App />)
   expect(screen.getByTestId('messages')).toBeInTheDocument()
   expect(screen.getByTestId('participants')).toBeInTheDocument()
   expect(screen.getByTestId('test-input')).toBeInTheDocument()
 })

 it('has calls being made', () => {
   render(<App />)
   expect(getParticipants).toHaveBeenCalled()
   expect(getMessages).toHaveBeenCalled()
 })

 describe('user is not logged in', () => {
   it('presents login modal', () => {
     render(<App />)
     expect(screen.getByTestId('login-modal')).toBeInTheDocument()
   })

   describe('user enters wrong credentials', () => {
     it('displays error message', async () => {
       mockLoginError()
       render(<App />)
       act(() => userLogIn('wronguser', 'xxx'))
       await screen.findByTestId('login-modal-error')
     })
   })

   describe('message input', () => {
     it('is disabled', () => {
       render(<App />)
       expect(screen.getByTestId('message-input-text')).not.toBeEnabled()
     })
   })

   describe('user logs in successfully', () => {
     it('hides login modal', async () => {
       mockLoginSuccess()
       render(<App />)
       act(() => userLogIn('rightuser', 'xxx'))
       await waitForElementToBeRemoved(() => screen.getByTestId('login-modal'))
     })
   })
 })

 describe('user is logged in', () => {
   describe('message input', () => {
     it('is enabled', async () => {
       mockLoginSuccess()
       render(<App />)
       act(() => userLogIn('rightuser', 'xxx'))
       await waitFor(() =>
         expect(screen.getByTestId('message-input-text')).toBeEnabled(),
       )
     })

     describe('submitting', () => {
       beforeEach(async () => {
         mockLoginSuccess()
         render(<App />)
         act(() => userLogIn('myuser', 'xxx'))
         await waitFor(() =>
           expect(screen.getByTestId('message-input-text')).toBeEnabled(),
         )
       })
       describe('without text', () => {
         it('does not send a message', async () => {
           userEvent.click(screen.getByTestId('message-input-button'))
           expect(postMessage).not.toHaveBeenCalled()
         })
       })

       describe('with text', () => {
         it('sends a message', async () => {
           const content = 'some message'
           act(() => {
             userEvent.type(screen.getByTestId('message-input-text'), content)
             userEvent.click(screen.getByTestId('message-input-button'))
           })

           await waitFor(() =>
             expect(postMessage).toHaveBeenCalledWith({
               user: currentUser,
               content,
             }),
           )
         })
       })
     })

     describe('participants', () => {
       beforeEach(() => {
         getParticipants.mockImplementationOnce(() =>
           Promise.resolve([
             { id: 0, name: 'Wanda', avatar: 'http://avatar.com/1' },
             { id: 1, name: 'Pietro', avatar: 'http://avatar.com/2' },
           ]),
         )
         render(<App />)
       })

       it('displays the result of the getParticipants call', async () => {
         const participants = screen.getByTestId('participants')
         await within(participants).findByText('Wanda')
         await within(participants).findByText('Pietro')
       })
     })

     describe('messages', () => {
       beforeEach(() => {
         getMessages.mockImplementationOnce(() =>
           Promise.resolve([
             { user: { id: 0, name: 'Pablo' }, content: 'some message' },
             {
               user: { id: 1, name: 'Ramona' },
               content: 'another message',
             },
           ]),
         )
         render(<App />)
       })

       it('displays the result of the getMessages call', async () => {
         const messages = screen.getByTestId('messages')
         await within(messages).findByText('some message')
         await within(messages).findByText('another message')
       })
     })
   })
 })
})

La filosofía que adoptamos consiste en mockear todas las llamadas a la API, ya que son dependencias y lo único que queremos comprobar es que se llega a hacer la llamada y a continuación darles una implementación específica para cada caso dependiendo de a qué resultados de la API queremos que reaccione la aplicación. Por último, simulamos la interacción del usuario y vemos si el resultado que obtenemos es el esperado.

Probamos desde el nivel de abstracción más alto, como es la interacción del usuario, hasta el nivel de abstracción más bajo, como es el resultado de llamar a las APIs (pero moqueadas para evitar dependencias externas). Los elementos de abstracción intermedio como los hooks debemos dejar sin mockear, ya que así no estaríamos probando su integración.

De nuevo hacemos una separación por estados y por la parte de funcionalidad que estamos probando:

¿aparecen todos los componentes?
¿se hacen las llamadas periódicas y automáticas que deberían hacerse?
con el usuario no logueado:
  ¿aparece el componente de login?
    introduciendo credenciales erróneas
      ¿aparece un mensaje de error?
    el input de mensajes
      ¿está desactivado?
    logueado el usuario
      el input de mensajes
        ¿se activa?
        enviando un mensaje
          sin texto
            ¿se envía?
          con texto
            ¿se envía?
      el listado de participantes
        ¿muestra el listado devuelto por el servicio de participantes?
    el listado de mensajes
      ¿muestra el resultado del servicio de mensajes?

Puede parecer que algunas de estas preguntas ya estaban contestadas con los tests unitarios pero no es así: con los unitarios probamos la interacción de todas las capas de la aplicación entre sí. Por ejemplo, el test unitario de LoginController comprueba si aparece la pantalla de login en base al valor almacenado del usuario actual (abstraído y mockeado en el hook useCurrentUser) sin embargo al probarlo en integración, no mockeamos el hook sino que simulamos diferentes escenarios con la respuesta del servicio y comprobamos si el dato pasa por todas las capas hasta mostrar o no la pantalla de login.

En este caso hemos abstraído la simulación de un usuario logueándose con unos datos parametrizados al ser una operación que hemos repetido varias veces y también hemos configurado la caché de react-query para que se resetee después de cada test (necesario sólo para proyectos con react-query).

// src/setupTests.js
import '@testing-library/jest-dom/extend-expect'
import userEvent from '@testing-library/user-event'
import { screen } from '@testing-library/react'
import { queryCache } from 'react-query'

// Setup for React-Query
afterEach(() => queryCache.clear())

export const userLogIn = (name, password) => {
 userEvent.type(screen.getByTestId('input-name'), name)
 userEvent.type(screen.getByTestId('input-password'), password)
 userEvent.click(screen.getByText(/log in/i))
}

Configuración y cobertura

Todo este proceso de testeo lo hemos llevado a cabo mientras se ejecutan los tests para poder ver el resultado de cada uno y ajustar dependiendo de si pasan o no sin errores con el conocido yarn test o npm test pero sabiendo la gran capacidad de configuración de Jest, podemos modificar la configuración y obtener además información sobre su cobertura, controlar la forma en que recibimos los resultados, imponer rangos mínimos de seguridad…

Podemos crear un archivo de configuración en la raíz del proyecto con las nuevas opciones de ejecución de Jest, extendiendo la configuración original de create-react-app e incluyendo nuevas opciones de cobertura.

// jest.config.js
process.env.BABEL_ENV = 'test'
process.env.NODE_ENV = 'test'
process.env.PUBLIC_URL = ''
require('react-scripts/config/env')

const path = require('path')
const createJestConfig = require('react-scripts/scripts/utils/createJestConfig')

module.exports = {
 ...createJestConfig(
   relativePath => require.resolve(path.join('react-scripts', relativePath)),
   __dirname,
   false,
 ),
 ...{
   // Esperamos cobertura de todos los archivos js, jsx, ts y tsx.
   // Excluimos la cobertura de los mocks de MSW (opcional) y de los índices.
   collectCoverageFrom: [
     'src/**/*.{js,jsx,ts,tsx}',
     '!src/mocks/**',
     '!src/serviceWorker.js',
     '!**/index.js',
   ],
   // Imponemos un mínimo del 90% de cobertura en las diferentes categorías de prueba.
   coverageThreshold: {
     global: {
       branches: 90,
       functions: 90,
       lines: 90,
       statements: 90,
     },
   },
   // Esperamos recibir el resultado como texto en el terminal.
   coverageReporters: ['text'],
 },
}

Ahora podemos modificar fácilmente los scripts en package.json para incluir uno adicional que nos dé cobertura además del original que solo ejecuta los tests, ambos utilizando el nuevo archivo de configuración.

// package.json
...
“scripts”: {
   // Antiguo: "test": "react-scripts test"
   "test": "react-scripts test --watch-all -- --config=jest.config.js",
   // Nuevo   
   "test:coverage": "react-scripts test --coverage --watch-all -- --config=jest.config.js",
}
...<br>

Ahora con yarn test:coverage o npm test:coverage obtendremos un resultado con la cobertura de cada archivo por sus tests asociados.

Resultado con la cobertura de cada archivo por sus tests asociados.

Como vemos, el informe incluye un resumen del porcentaje de cobertura, fallos y si se cumple el margen de seguridad mínimo. Es un arduo camino hacia el 100% pero organizándonos bien y con estas técnicas podemos acercarnos bastante.

En resumen, React Testing Library nos ofrece un nuevo enfoque a más alto nivel sobre los tests, válido tanto para unitarios como para integración y al combinarlos podemos conseguir una cobertura de tests con la que que confirmar que estamos desarrollando sobre seguro en proyectos de cualquier tamaño.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.