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
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
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>
)
}
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'
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', () => {})
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', () => {})
})
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 />)
})
})
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')
})
})
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)
})
})
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')
})
})
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'
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>
)
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'
Y el siguiente paso será proporcionárlo al Provider
en su prop store
:
render(
<Provider store={store}>
<ReduxCounter />
</Provider>
)
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')
})
})
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')
})
})
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
}
})
y la creación del store
ahora simplemente se conseguirá invocando a dicha función:
export const store = createStore()
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
¿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'
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')
})
})
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')
})
})
Top comments (0)