Metamorfosis de la prueba de la saga redux

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:

  1. El usuario presiona un botón.
  2. Se envía una solicitud al servidor, informando que el usuario ha pulsado un botón.
  3. El servidor devuelve el número de clics realizados.
  4. El estado registra el número de clics realizados.
  5. La IU se actualiza y el usuario ve que la cantidad de clics ha aumentado.
  6. ...
  7. 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 .

Source: https://habr.com/ru/post/458500/


All Articles