Pruebas unitarias en Redux.js

Cuando desarrollamos una aplicación con Redux.js la mayor parte del código que escribas van a ser funciones puras, esto hace que crear pruebas unitarias para nuestra aplicación sea más fácil que nunca.

Preparando el ambiente de pruebas

Lo primero que necesitamos para empezar a hacer pruebas es configurar nuestro ambiente de desarrollo local para correr las pruebas. Para esto vamos a usar las librerías tape y tap-spec.

npm i -D tape tap-spec
  • tape: nos sirve para realizar nuestras pruebas
  • tap-spec: formatea y colorea el resultado de las pruebas en consola.

Para usarlos vamos a crear una carpeta llamada test y dentro un index.js que es nuestro entry point para pruebas. Luego vamos a crear un script en package.json llamado test.

{
...
"scripts": {
"test": "tape -r babel-core/register test/index.js | tap-spec"
}
..
}

Como se ve vamos a estar usando Babel.js para transpilar el código de nuestras pruebas. Cuando vayamos a correr nuestras pruebas simplemente usamos el comando de npm.

npm test

Y con esto ya ejecutamos las pruebas.

Creadores de acciones

Un creador de acciones es simplemente una función pura que recibe ciertos parámetros y devuelve un objeto que describe una acción. Por ejemplo.

export default function addTodo(message) {
return {
type: 'ADD_TODO',
payload: {
message,
},
};
}

Ese es un ejemplo de un creador de acciones común. Vamos a crearle una prueba.

import test from 'tape';
import addTodo from '../actions/add-todo';
test('ADD_TODO action creator', t => {
t.plan(1);
  t.deepEquals(addTodo('hello world'), {
type: 'ADD_TODO',
payload: {
message: 'hello world',
},
}, 'it should return the expected object');
});

Con esto ya tenemos una prueba para verificar que nuestro creador de acciones funciona correctamente. En general todos los creadores de acciones van a funcionar de esta forma así que probarlos es bastante sencillo.

Reducers

Los Reducers son la parte más importante de cada aplicación de Redux, y deberían todos tener pruebas unitarias para asegurarse su correcto funcionamiento. Un ejemplo simple de un reducer puede ser el siguiente.

// definimos el estado inicial
const initialState = [];

function todos(state = initialState, action = {}) {
switch (action.type) {
// si la acción es ADD_TODO
case 'ADD_TODO':
// copiamos el estado actual
const newState = Array.from(state);
      // agregamos el mensaje nuevo
newState.push(action.payload.message);
      // devolvemos el nuevo estado
return newState;
    // si no identificamos la acción
default:
// devolvemos el estado sin tocarlo
return state;
}
}

export default todos;

Para poder hacer pruebas sobre un reducer necesitamos simplemente ejecutarlo pasándole un estado y una acción y ver el resultado que devuelve.

import test from 'tape';
import addTodo from '../actions/add-todo';
import todos from '../reducers/todos';
test('todos reducer', t => {
t.plan(2);
  const defaultState = todos();
t.deepEquals(
defaultState,
[],
'it should return an empty array as default'
);
  const message = 'Hello world';
const state = todos(defaultState, addTodo(message));
t.deepEquals(
state,
[message],
'it should return an array with the message'
);
});

Con esto ya probamos que ocurre cuando no recibe una acción y que ocurre cuando recibe una acción de tipo ADD_TODO. Gracias a estas dos simples pruebas podemos asegurarnos de que nuestro reducer funciones correctamente.

Middlewares

Acá ya se puede volver más complicado de probar, principalmente porque depende del middleware que estemos probando el como vamos a escribir nuestras pruebas. Para este ejemplo vamos a usar el middleware socket.io-redux. Primero veamos el código del middleware.

const socketIO = socket => () => next => action => {
if (action.meta && action.meta.socket && action.meta.socket.channel) {
socket.emit(action.meta.socket.channel, action);
}
return next(action);
};
export default socketIO;

Ese es el código de nuestro middleware, básicamente recibe una acción y valida que tenga la propiedad meta, que esta sea un objeto con una propiedad socket que a su vez sea otro objeto con la propiedad channel. Si posee todo esto entonces emite la acción a través del canal especificado en la metadata de la acción. Por último pasa la acción al siguiente middleware o al reducer.

Ahora veamos como hacer una prueba de este middleware.

import test from 'tape';
import socketIO from 'socket.io-redux';
// simulamos la función next que va a usar el middleware
function next(action) { return action; }
test('socket.io middleware', t => {
t.plan(3);
  // nuestra acción de prueba con los datos necesarios
const testAction = {
type: 'ADD_NEW',
payload: 'hello world!,
meta: { socket: { channel: 'add:new' } },
};
  // simulamos el objeto socket con el método emit
const socket = {
emit(channel, data) {
// nuestro falso método emit
// acá hacemos las pruebas
t.equals(
channel,
'add:new',
'it should have the channel "add:new"'
);
t.deepEquals(
data,
testAction,
'it should have the action as data'
);
},
};
  // ejecutamos el middleware y guardamos el resultado
const action = socketIO(socket)()(next)(testAction);
  t.deepEquals(
action,
testAction,
'it should return the passed action'
);
});

Conclusiones

Hacer pruebas unitarias de nuestro código es super importante para evitarnos problemas y encontrar errores más rápido.

En el caso de aplicaciones de Redux ya que la mayor parte de nuestro código no depende directamente de Redux para funcionar es muy simple hacer pruebas unitarias en este por lo que hay no excusa para no hacer pruebas y ser mejores desarrolladores.