Pengujian Unit dan Integrasi di Redux Saga dengan Contoh

gambar pahlawan



Redux adalah pengelola negara yang sangat berguna. Di antara banyak "plugin", Redux-Saga adalah favorit saya. Pada proyek React-Native yang sedang saya kerjakan, saya harus berurusan dengan banyak efek samping. Mereka akan membuat saya sakit kepala jika saya memasukkannya ke dalam ramuan. Dengan alat ini, membuat alur logis percabangan yang kompleks menjadi tugas yang sederhana. Tapi bagaimana dengan pengujian? Semudah menggunakan perpustakaan? Meskipun saya tidak dapat memberikan jawaban yang tepat, saya akan menunjukkan kepada Anda contoh nyata dari masalah yang saya hadapi.



Jika Anda tidak terbiasa dengan pengujian saga, saya sarankan membaca halaman terpisah di dokumentasi. Dalam contoh berikut saya gunakan redux-saga-test-plankarena pustaka ini memberikan kekuatan penuh pengujian integrasi bersama dengan pengujian unit.



Sedikit tentang pengujian unit



Pengujian unit tidak lebih dari menguji sebagian kecil dari sistem Anda , biasanya sebuah fungsi, yang perlu diisolasi dari fungsi lain dan yang lebih penting dari 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