Pruebas de unidad e integración en Redux Saga por ejemplo

imagen de héroe



Redux es un administrador de estado extremadamente útil. Entre los muchos "complementos", Redux-Saga es mi favorito. En un proyecto de React-Native en el que estoy trabajando actualmente, tuve que lidiar con muchos efectos secundarios. Me darían dolores de cabeza si los pongo en ingredientes. Con esta herramienta, crear flujos lógicos de ramificación complejos se convierte en una tarea sencilla. Pero, ¿qué pasa con las pruebas? ¿Tan fácil como usar una biblioteca? Si bien no puedo darte una respuesta exacta, te mostraré un ejemplo de la vida real de los problemas a los que me enfrento.



Si no está familiarizado con las pruebas de sagas, le recomiendo leer una página separada en la documentación. En los siguientes ejemplos que estoy usando redux-saga-test-plan, esta biblioteca brinda todo el poder de las pruebas de integración junto con las pruebas unitarias.



Un poco sobre las pruebas unitarias



Las pruebas unitarias no son más que probar una pequeña parte de su sistema , generalmente una función, que debe aislarse de otras funciones y, lo que es más importante, de la API.



, . - API , . , , , , ( ).


//    
import {call, put, take} from "redux-saga/effects";

export function* initApp() {
    //    
    //    
    yield put(initializeStorage());
    yield take(STORAGE_SYNC.STORAGE_INITIALIZED);

    yield put(loadSession());
    let { session } = yield take(STORAGE_SYNC.STORAGE_SESSION_LOADED);

    //   
    if (session) {
        yield call(loadProject, { projectId: session.lastLoadedProjectId });
    } else {
        logger.info({message: "No session available"});
    }
}


//    
import {testSaga} from "redux-saga-test-plan";

it("      `loadProject`", () => {
    const projectId = 1;
    const mockSession = {
        lastLoadedProjectId: projectId
    };

    testSaga(initApp)
        // `next`       `yield`
        //      ,
        //      `yield`

        //       
        //(   -  )
        .next()
        .put(initializeStorage())

        .next()
        .take(STORAGE_SYNC.STORAGE_INITIALIZED)

        .next()
        .put(loadSession())

        .next()
        .take(STORAGE_SYNC.STORAGE_SESSION_LOADED)

        //  ,    
        .save(" ")

        //  ,     `yield take...`
        .next({session: mockSession})
        .call(loadProject, {projectId})

        .next()
        .isDone()

        //    
        .restore(" ")

        // ,    ,
        //     
        .next({})
        .isDone();
});


. - API, , jest.fn.



, !





. , . , , , , . , , ? , (reducers)? , .





, :



//    
import {call, fork, put, take, takeLatest, select} from "redux-saga/effects";

//  
export default function* sessionWatcher() {
    yield fork(initApp);
    yield takeLatest(SESSION_SYNC.SESSION_LOAD_PROJECT, loadProject);
}

export function* initApp() {
    //       
    yield put(initializeStorage());
    yield take(STORAGE_SYNC.STORAGE_INITIALIZED);

    yield put(loadSession());
    let { session } = yield take(STORAGE_SYNC.STORAGE_SESSION_LOADED);

    //   
    if (session) {
        yield call(loadProject, { projectId: session.lastLoadedProjectId });
    } else {
        logger.info({message: "  "});
    }
}

export function* loadProject({ projectId }) {
    //        
    yield put(loadProjectIntoStorage(projectId));
    const project = yield select(getProjectFromStorage);

    //  ,        
    try {
        yield put({type: SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC, project});
        yield fork(saveSession, projectId);
        yield put(loadMap());
    } catch(error) {
        yield put({type: SESSION_SYNC.SESSION_ERROR_WHILE_LOADING_PROJECT, error});
    }
}

export function getProjectFromStorage(state) {
    //      
}

export function* saveSession(projectId) {
    // ....   API
    yield call(console.log, " API...");
}


sessionWatcher, , initApp , id. , , . , :



  • API, .


//    
import { expectSaga } from "redux-saga-test-plan";
import { select } from "redux-saga/effects";
import * as matchers from "redux-saga-test-plan/matchers";

it("         ", () => {
    //  
    const projectId = 1;
    const anotherProjectId = 2;
    const mockedSession = {
        lastLoadedProjectId: projectId,
    };
    const mockedProject = "project";

    //  `sessionWatcher`
    // `silentRun`         
    //      
    return (
        expectSaga(sessionWatcher)
            //   
            .provide([
                //    `select` ,  
                // `getProjectFromStorage`      `mockedProject`
                //            ,
                //      `select`,
                //       

                //     
                //  Redux-Saga,  
                [select(getProjectFromStorage), mockedProject],

                //    `fork` ,   `saveSession` 
                //     (undefined)
                //        ,
                //  

                //     Redux Saga Test Plan
                [matchers.fork.fn(saveSession)],
            ])

            //    
            //      ,    

            //  
            .put(initializeStorage())
            .take(STORAGE_SYNC.STORAGE_INITIALIZED)
            //  ,       `take`  `initApp`
            //       
            .dispatch({ type: STORAGE_SYNC.STORAGE_INITIALIZED })

            .put(loadSession())
            .take(STORAGE_SYNC.STORAGE_SESSION_LOADED)
            .dispatch({ type: STORAGE_SYNC.STORAGE_SESSION_LOADED, session: mockedSession })

            //   ,  `initApp`
            .put(loadProjectFromStorage(projectId))
            .put({ type: SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC, project: mockedProject })
            .fork(saveSession, projectId)
            .put(loadMap())

            //  ,    `takeLatest`  `sessionWatcher`
            //     
            //   ,  `sessionWatcher`
            .dispatch({ type: SESSION_SYNC.SESSION_LOAD_PROJECT, projectId: anotherProjectId })
            .put(loadProjectFromStorage(anotherProjectId))
            .put({ type: SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC, project: mockedProject })
            .fork(saveSession, anotherProjectId)
            .put(loadMap())

            //  ,      
            .silentRun()
    );
});


. , , — . waitSaga, .



, , — provide , . ( ) select Redux Saga , getProjectFromStorage. , , Redux Saga Test Plan. , , saveSession, . , API.



. , , , . (dispatch) .



silentRun, : , - , .





, provide redux-saga-test-plan/providers, .



//    
import {expectSaga} from "redux-saga-test-plan";
import {select} from "redux-saga/effects";
import * as matchers from "redux-saga-test-plan/matchers";
import * as providers from "redux-saga-test-plan/providers";

it("       ", () => {
    const projectId = 1;
    const mockedSession = {
        lastLoadedProjectId: projectId
    };
    const mockedProject = "project";
    const mockedError = new Error(",  -   !");

    return expectSaga(sessionWatcher)

        .provide([
            [select(getProjectFromStorage), mockedProject],
            //    
            [matchers.fork.fn(saveSession), providers.throwError(mockedError)]
        ])

        //  
        .put(initializeStorage())
        .take(STORAGE_SYNC.STORAGE_INITIALIZED)
        .dispatch({type: STORAGE_SYNC.STORAGE_INITIALIZED})

        .put(loadSession())
        .take(STORAGE_SYNC.STORAGE_SESSION_LOADED)
        .dispatch({type: STORAGE_SYNC.STORAGE_SESSION_LOADED, session: mockedSession})

        //   ,  `initApp`
        .put(loadProjectFromStorage(projectId))
        .put({type: SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC, project: mockedProject})
        //    
        .fork(saveSession, projectId)
        // ,    
        .put({type: SESSION_SYNC.SESSION_ERROR_WHILE_LOADING_PROJECT, error: mockedError})

        .silentRun();
});




, , (reducers). redux-saga-test-plan . -, :



const defaultState = {
    loadedProject: null,
};

export function sessionReducers(state = defaultState, action) {
    if (!SESSION_ASYNC[action.type]) {
        return state;
    }
    const newState = copyObject(state);

    switch(action.type) {
        case SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC: {
            newState.loadedProject = action.project;
        }
    }

    return newState;
}


-, , withReducer, ( , withState). hasFinalState, .



//    
import {expectSaga} from "redux-saga-test-plan";
import {select} from "redux-saga/effects";
import * as matchers from "redux-saga-test-plan/matchers";

it("         ", () => {
    const projectId = 1;
    const mockedSession = {
        lastLoadedProjectId: projectId
    };
    const mockedProject = "project";
    const expectedState = {
        loadedProject: mockedProject
    };

    return expectSaga(sessionWatcher)
        //     , 
        //          `withState`
        .withReducer(sessionReducers)

        .provide([
            [select(getProjectFromStorage), mockedProject],
            [matchers.fork.fn(saveSession)]
        ])

        //  
        .put(initializeStorage())
        .take(STORAGE_SYNC.STORAGE_INITIALIZED)
        .dispatch({type: STORAGE_SYNC.STORAGE_INITIALIZED})

        .put(loadSession())
        .take(STORAGE_SYNC.STORAGE_SESSION_LOADED)
        .dispatch({type: STORAGE_SYNC.STORAGE_SESSION_LOADED, session: mockedSession})

        //   ,  `initApp`
        .put(loadProjectFromStorage(projectId))

        //      ,   ,
        //       
        // .put({type: SESSION_ASYNC.SESSION_PROJECT_LOADED_ASYNC, project: mockedProject})
        .fork(saveSession, projectId)
        .put(loadMap())

        //   
        .hasFinalState(expectedState)

        .silentRun();
});


Medium.



. , .




All Articles