El marco
redux-saga
proporciona un montón de patrones interesantes para trabajar con efectos secundarios, pero, al igual que los verdaderos desarrolladores empresariales sangrientos, necesitamos cubrir todo nuestro código con pruebas. Veamos cómo vamos a probar nuestras sagas.

Tome el clicker más simple como ejemplo. El flujo de datos y el significado de la aplicación serán los siguientes:
- El usuario presiona un botón.
- Se envía una solicitud al servidor, informando que el usuario ha pulsado un botón.
- El servidor devuelve el número de clics realizados.
- El estado registra el número de clics realizados.
- La IU se actualiza y el usuario ve que la cantidad de clics ha aumentado.
- ...
- BENEFICIOS
En nuestro trabajo, usamos Typecript, por lo que todos los ejemplos estarán en este idioma.
Como probablemente ya haya adivinado, implementaremos todo esto con
redux-saga
. Aquí está el código para todo el archivo sagas:
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) }
En este sencillo ejemplo, declaramos el proceso de saga
processClick
, que procesa directamente la acción y el
watchClick
saga
watchClick
, que crea un ciclo para procesar
action'
.
Generadores
Entonces tenemos la saga más simple. Envía una solicitud al servidor
( call)
, recibe el resultado y lo pasa al reductor
( put)
. Necesitamos probar de alguna manera si la saga está transmitiendo exactamente lo que recibe del servidor. Empecemos
Para las pruebas, necesitamos bloquear la llamada al servidor y de alguna manera verificar si exactamente lo que vino del servidor entró en el reductor.
Como las sagas son funciones generadoras, el método
next()
, que se encuentra en el prototipo del generador, será la forma más obvia de probar. Al utilizar este método, tenemos la oportunidad de recibir el siguiente valor del generador y transferir el valor al generador. Por lo tanto, sacamos de la caja la oportunidad de recibir llamadas mojadas. ¿Pero todo es tan color de rosa? Aquí hay una prueba que escribí en generadores desnudos:
it('should increment click counter (behaviour test)', () => { const saga = processClick() expect(saga.next().value).toEqual(call(ServerApi.SendClick)) expect(saga.next(10).value).toEqual(put(Actions.clickSuccess(10))) })
La prueba fue concisa, pero ¿qué prueba? De hecho, simplemente repite el código del método de la saga, es decir, cualquier cambio en la saga tendrá que cambiar la prueba.
Tal prueba no ayuda en el desarrollo.
Redux-saga-test-plan
Después de encontrar este problema, decidimos buscarlo en Google y de repente nos dimos cuenta de que no éramos los únicos y estábamos lejos de ser los primeros. Directamente
en la documentación de
redux-saga
desarrolladores ofrecen un vistazo a varias bibliotecas creadas específicamente para satisfacer a los fanáticos de las pruebas.
De la lista propuesta tomamos la biblioteca
redux-saga-test-plan
. Aquí está el código para la primera versión de la prueba que escribí con él:
it('should increment click counter (behaviour test with test-plan)', () => { return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 2] ]) .dispatch(Actions.click()) .call(ServerApi.SendClick) .put(Actions.clickSuccess(2)) .run() })
El constructor de prueba en
redux-saga-test-plan
es la función
expectSaga
, que devuelve la interfaz que describe la prueba. La saga de prueba (
processClick
de la primera lista) se pasa a la función misma.
Usando el método
provide
, puede bloquear las llamadas al servidor u otras dependencias. Se
StaticProvider'
una matriz de
StaticProvider'
, que describen qué método debe devolver.
En el bloque
Act
, tenemos un único método:
dispatch
. Se le pasa una acción, a lo que la saga responderá.
El bloque de
assert
consiste en los métodos
call put
, que verifican si los efectos correspondientes fueron causados durante el trabajo de la saga.
Todo termina con el método
run()
. Este método ejecuta la prueba directamente.
Las ventajas de este enfoque:
- Comprueba si se llamó al método y no la secuencia de llamadas;
- moki describe claramente qué función se moja y qué regresa.
Sin embargo, hay trabajo por hacer:
- hay más código;
- el examen es difícil de leer;
- Esta es una prueba de comportamiento, lo que significa que todavía está conectado con la implementación de la saga.
Los dos últimos golpes
Prueba de condición
Primero, arreglamos el último: hacemos una prueba de estado a partir de una prueba de comportamiento. El hecho de que
test-plan
nos permita establecer el
state
inicial y el
reducer
paso, que debería responder a los efectos generados por la saga, nos ayudará con esto. Se ve así:
it('should increment click counter (state test with test-plan)', () => { const initialState = { clickCount: 11, return expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) .dispatch(Actions.click()) .run() .then(result => expect(result.storeState.clickCount).toBe(14)) })
En esta prueba, ya no verificamos que se haya activado ningún efecto. Verificamos el estado final después de la ejecución, y eso está bien.
Logramos deshacernos de la implementación de la saga, ahora intentemos hacer la prueba más comprensible. Esto es fácil si reemplaza
then()
con
async/await
:
it('should increment click counter (state test with test-plan async-way)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(processClick) .provide([ call(ServerApi.SendClick), 14] ]) .withReducer(rootReducer, initialState) const result = await saga.dispatch(Actions.click()).run() expect(result.storeState.clickCount).toBe(14) })
Pruebas de integración
Pero, ¿qué pasa si también tenemos una operación de clic inverso (llamémosla unclick), y ahora nuestro archivo de caída se ve así:
export function* processClick() { const result = yield call(ServerApi.SendClick) yield put(Actions.clickSuccess(result)) } export function* processUnclick() { const result = yield call(ServerApi.SendUnclick) yield put(Actions.clickSuccess(result)) } function* watchClick() { yield takeEvery(ActionTypes.CLICK, processClick) } function* watchUnclick() { yield takeEvery(ActionTypes.UNCLICK, processUnclick) } export default function* mainSaga() { yield all([watchClick(), watchUnclick()]) }
Supongamos que necesitamos probar que cuando las acciones de hacer clic y dejar de hacer clic se llaman en estado, el resultado del último viaje al servidor se escribe en estado. Tal prueba también se puede hacer fácilmente con
redux-saga-test-plan
:
it('should change click counter (integration test)', async () => { const initialState = { clickCount: 11, } const saga = expectSaga(mainSaga) .provide([ call(ServerApi.SendClick), 14], call(ServerApi.SendUnclick), 18] ]) .withReducer(rootReducer, initialState) const result = await saga .dispatch(Actions.click()) .dispatch(Actions.unclick()) .run() expect(result.storeState.clickCount).toBe(18) })
Tenga en cuenta que ahora estamos probando
mainSaga
, y no manejadores de
mainSaga
individuales.
Sin embargo, si ejecutamos esta prueba tal cual, obtendremos el Vorning:

Esto se debe al efecto
takeEvery
: este es un ciclo de procesamiento de mensajes que funcionará mientras nuestra aplicación esté abierta. En consecuencia, la prueba en la que se
takeEvery
no podrá completar el trabajo sin ayuda externa, y
redux-saga-test-plan
termina por la fuerza estos efectos después de 250 ms después del inicio de la prueba. Este tiempo de espera se puede cambiar llamando a expectSaga.DEFAULT_TIMEOUT = 50.
Si no desea recibir tales vorings, uno para cada prueba con un efecto complejo, simplemente use el método silentRun()
lugar del método run()
.
Trampas
Donde sin dificultades ... En el momento de escribir este artículo, la última versión de redux-saga: 1.0.2. Al mismo tiempo,
redux-saga-test-plan
puede trabajar con él en JS.
Si desea TypeScript, debe instalar la versión del canal beta:
npm install redux-saga-test-plan@beta
y apague las pruebas de la compilación. Para hacer esto, en el archivo tsconfig.json, debe especificar la ruta "./src/**/*.spec.ts" en el campo "excluir".
A pesar de esto, consideramos que
redux-saga-test-plan
la mejor biblioteca para probar
redux-saga
. Si tiene
redux-saga
en su proyecto, tal vez sea una buena opción para usted.
El código fuente del ejemplo en
GitHub .