Test componentes que usan Redux
Tags: spanish, testing, react, redux
Published: 2024-05-01 07:08:18

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:

```tsx
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í](https://redux-toolkit.js.org/tutorials/quick-start).

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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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):

```tsx
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:

```tsx
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:

```tsx
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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z7s14r6ta29wn378cwnd.png)

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:

```tsx
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`):

```tsx
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_:

```tsx
import { store } from './store'
```

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

```tsx
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:

```tsx
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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rsez97qc1hrxm9hkzhbp.png)

## 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:

```tsx
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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r5duu73i877tihffqfv3.png)

¿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:

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

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

```tsx
export const store = createStore()
```

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

```tsx
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`:

```tsx
import { createStore } from './store'
```

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

```tsx
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:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ox6isi4masjcetqq9zod.png)

> **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:

```tsx
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')
  })
})
```