DEV Community

Cover image for Test componentes que usan Redux
DevJoseManuel
DevJoseManuel

Posted on

Test componentes que usan Redux

En este artículo vamos a ver los pasos que tenemos que dar para poder testear un componente que esté usando un store de Redux y para ello la mejor forma de hacerlo es mostrando un componente que esté haciendo uso de Redux. Así pues supongamos que en nuestro proyecto tenemos un fichero denominado counterSlice.ts y dentro del mismo vamos a escribir el siguiente código:

import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: state => {
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
Enter fullscreen mode Exit fullscreen mode

Sin entrar en demasiados detalles del código anterior simplemente mencionar que estamos creando un slice gracias a Redux Toolkit que se denomina counter y este slice lo que hace es guardar la información de un objeto que tiene un único atributo denominado value de tipo númerico. Además, el slice nos ofrece laas posibilidades de incrementar en uno, decrementar en uno o incrementar en una cantidad que nosotros queramos el valor de este contador gracias al uso de los acciones increment, decrement e incrementByAmount respectivamente.

** Nota:** el código anterior está sacado de la documentación de Redux Toolkit. Si se quiere obtener más información de qué representa y cómo funciona se recomienda acudir a la esa misma documentación aquí.

Además vamos a tene definido el fichero store.ts en el que se llevarán a cabo todas las operaciones que nos van a permitir inicializar Redux en nuestra aplicación y cuyo código sería algo como lo siguiente:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
})

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Enter fullscreen mode Exit fullscreen mode

Componente

Una vez tenemos el store de Redux creado el siguiente paso que tenemos que dar consistirá en crear el componente de React que pase a usarlo. Así pues creamos el fichero ReduxCounter.tsx cuyo código será el que podemos ver a continuación:

import { useSelector, useDispatch } from 'react-redux'
import { RootState, decrement, increment } from './counterSlice'

export const ReduxCounter = () => {
  const count = useSelector((state: RootState) => state.counter)
  const dispatch = useDispatch()

  return (
    <div>
      <button
        aria-lable="Increment value"
        onClick={() => dispatch(increment())}
      >
        Increment
      </button>
      <span role="contentinfo">{count}</span>
      <button
        aria-lable="Decrement value"
        onClick={() => dispatch(decrement())}
      >
        Decrement
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

En el código anterior lo que estamos haciendo es traernos los hook useSelector y useDispatch que son propios de Redux además de las acciones que han sido definidas dentro del slice counterSlice de tal manera que obtenemos el valor actual del contados que está recogido en Redux además de utilizar dispatch para poder ejectar las acciones sobre el store de Redux cuando se pulse sobre los botones para incrementar o decrementar el valor del contador.

Test

La pregunta que surge ahora es ¿qué tenemos que hacer para poder testear nuestro componente? Para ello lo que vamos a hacer es crear el fichero ReduxCounter.test.tsx y dentro del mismo lo que haremos será importarnos todas aquellas funciones que vamos a necesitar desde la React Testing Library así como el código de componente que vamos a probar:

import { render, screen, fireEvent } from '@testing-library/react'
import { ReduxCounter } from './ReduxCounter'
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a definir el test que queremos llevar a cabo en el código y para ello comenzaremos definiendo una nueva suite de test:

describe('ReduxCounter', () => {})
Enter fullscreen mode Exit fullscreen mode

y dentro de la misma comenzaremos escribiendo el primero de nuestros test que será el encargado de comprobar que al pulsar sobre el botón Increment se estará incrementando el valor del contador:

describe('ReduxCounter', () => {
  it('increment', () => {})
})
Enter fullscreen mode Exit fullscreen mode

Bien, como sucede con cualquier otro componente en React cuando lo estemos probando lo primero que vamos a tener que hacer es renderizarlo gracias al uso de la función render que nos proporciona React Testing Library:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(<ReduxCounter />)
  })
})
Enter fullscreen mode Exit fullscreen mode

Hecho esto lo siguiente que vamos a hacer es obtener el contenido del <span> al que le hemos asignado el role de contentinfo con el fin de asegurar que en el momento en el que se está renderizando el componente este tendrá el valor 0 (es decir, que 0 es el valor inicial de partida para el contador):

describe('ReduxCounter', () => {
  it('increment', () => {
    render(<ReduxCounter />)

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')
  })
})
Enter fullscreen mode Exit fullscreen mode

El siguiente paso que daremos será obtener el botón que nos permitirá incrementar el valor de nuestro contador y pulsarlo:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(<ReduxCounter />)

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)
  })
})
Enter fullscreen mode Exit fullscreen mode

y si todo funciona tal cual esperaríamos al pulsar sobre este botón Increment el nuevo valor del contador debería ser 1 puesto que se habrá incrementado en una unidad por lo que escribimos la aserción que se encargará de realizar esta comprobación:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(<ReduxCounter />)

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })
})
Enter fullscreen mode Exit fullscreen mode

Nota: el código con el que hemos estado trabajando no cumple el formato de test AAA (Arrange-Action-Assert) porque estamos mezclando cada una de estas secciones pero está hecho así a posta para no alargar demasiado la explicación. Lo correcto sería que tuviésemos más test en nuestro código como por ejemplo uno que asegurase que el valor inicial del contador es 0 y no tener que realizar la aserción en el medido de nuestros test como hemos hecho en el código que hemos ido desarrollando.

Si guardamos nuestro trabajo y ejecutamos este test en la consola nos vamos a encontrar con un error como el que se puede ver en la siguiente imagen:

es decir que no se ha podido renderizar el componente pueto que cualquier componente que esté haciendo uso de Redux siempre va a tener que tener asociado un Provider que le proporcione el acceso al mismo.

Por lo tanto la solución a este problema pasará por proporcionárselo al componente y para ello lo primero que tendremos que hacer será importar el componente Provider de la libraría react-redux como sigue:

import { Provider } from 'react-redux'
Enter fullscreen mode Exit fullscreen mode

lo que hace que a continuación envolvamos el componente ReduxCounter dentro del mismo (o dicho de otra manera, haremos que ReduxCounter pase a ser un children de Provider):

render(
  <Provider>
    <ReduxCounter />
  </Provider>
)
Enter fullscreen mode Exit fullscreen mode

Ahora bien, en este momento tenemos que saber que el Provider de Redux precisa recibir como prop el store con el que estará trabajando por lo que vamos también a importar nuestro store:

import { store } from './store'
Enter fullscreen mode Exit fullscreen mode

Y el siguiente paso será proporcionárlo al Provider en su prop store:

render(
  <Provider store={store}>
    <ReduxCounter />
  </Provider>
)
Enter fullscreen mode Exit fullscreen mode

Al final esto nos deja de la definición del test para probrar la funcionalidad asociada al botón Increment tal y como se puede ver a continuación:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(
      <Provider store={store}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })
})
Enter fullscreen mode Exit fullscreen mode

De tal manera que si ahora guardamos nuestro trabajo y volvemos a ejecutar los test obtendremos que todos ellos pasarán correctamente tal y como esperábamos:

Problemas adicionales

Con todo esto parece que hemos logrado resolver nuestro problema pero es un poco más complicado puesto que tenemos que saber que Redux es una parte de nuestra aplicación que es global a toda ella lo que quiere decir que con lo que acabamos de hacer en el test anterior lo que hemos logrado es que el valor del contador dentro del store de Redux tenga el valor 1 incluso fuera del test que acabamos de crear.

¿Qué queremos decir con esto? Pues la mejor manera es verlo nuevamente con un ejemplo y para ello supongamos que duplicamos el test que acabamos de crear pero esta vez lo llamamos increment again dejando algo como lo siguiente:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(
      <Provider store={store}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })

  it('increment again', () => {
    render(
      <Provider store={store}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })
})
Enter fullscreen mode Exit fullscreen mode

Pues aunque nos pueda parecer lo contrario estará fallando la primera de las dos aserciones que estamos realizando en el segundo de los test puesto que estamos intentando comprobar que el contenido del contador cuando comienza la ejecución de nuestro test es 0 y sin embargo tiene el valor 1 puesto que se ha visto incrementado tras la ejecución del primero de nuestros test:

¿Qué solución tenemos para ello? Pues para ello vamos a hacer una pequeña modificación en el fichero store.ts de tal manera que asígnaremos la función configureStore con los parámetros que crean nuestro store de Redux a una variable:

export const createStore = configureStore({
  reducer: {
    counter: counterReducer
  }
})
Enter fullscreen mode Exit fullscreen mode

y la creación del store ahora simplemente se conseguirá invocando a dicha función:

export const store = createStore()
Enter fullscreen mode Exit fullscreen mode

Esto nos deja el código del fichero store.ts como se puede ver en la siguiente imagen:

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const createStore = configureStore({
  reducer: {
    counter: counterReducer
  }
})

export const store = createStore()

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Enter fullscreen mode Exit fullscreen mode

¿Cuál es el objetivo de todo esto? Pues que en el momento en el que estemos asignando la prop store del Provider de Redux lo que vamos a hacer es crear un nuevo store cada vez que se vaya a ejecutar cada uno de los test (o dicho de otra manera, en cada uno de los test que necesitan del Provider de Redux lo que vamos a hacer es crear un store nuevo).

Así pues en nuestro test importaremos la función createStore:

import { createStore } from './store'
Enter fullscreen mode Exit fullscreen mode

y ahora pasamos a usarla en los test que hemos definido lo que nos deja algo como lo siguiente:

describe('ReduxCounter', () => {
  it('increment', () => {
    render(
      <Provider store={createStore()}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })

  it('increment again', () => {
    render(
      <Provider store={createStore()}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })
})
Enter fullscreen mode Exit fullscreen mode

Si ahora guardamos el código de nuestros test podremos comprobar como ambos pasarán correctamente tal y como esperábamos:

Nota: No desarrollamos los test para el botón Decrement porque el razonamiento que se ha de seguir es exactamente el mismo que hemos hecho para Increment y así no alargaremos más la explicación.

Código completo

El código completo de nuestro test es el que podemos ver a continuación:

import { render, screen, fireEvent } from '@testing-library/react'
import { Provider } from 'react-redux'
import { createStore } from './store'
import { ReduxCounter } from './ReduxCounter'

describe('ReduxCounter', () => {
  it('increment', () => {
    render(
      <Provider store={createStore()}>
        <ReduxCounter />
      </Provider>
    )

    const counter = screen.getByRole('contentinfo')
    expect(counter).toHaveTextContent('0')

    const addButton = screen.getByText(/Increment/i)
    fireEvent.click(addButton)

    expect(counter).toHaveTextContent('1')
  })
})
Enter fullscreen mode Exit fullscreen mode

Top comments (0)