preface

In order to ensure the quality of the software and the correctness of the functional logic, we usually do unit testing. This paper mainly describes how to unit test Reducer and effect in model in the application based on DVA framework, as well as the principle behind it.

Dva framework

This is a lightweight data flow framework developed by domestic developers. It integrates popular JavaScript libraries such as Redux, React-Router and Redux-Saga. It is easy to learn and easy to use, making data flow management easier and more efficient.

Test reducers

Because reducer is a pure function, as long as a payload is given, there will be a certain output and the output result will not be changed due to the influence of other external environments of the function.

A reducer function example

For example, I have a reducer function saveNotify that updates notification messages from payload to state:

saveNotify(state, { payload }) {
    let {notification} = state
    if(! payload) {returnstate } notification = {... notification, ... payload.notification} notification.visible =true
    state.notification = notification
}
Copy the code

Note that we can configure the umi-plugin-react property in the.umirc.js file and set the dVA on immer to true, thus preserving the original state.

[
    'umi-plugin-react',
      {
        dva: {
          immer: true}}]Copy the code

This is why we can directly manipulate the incoming state and change its value, because the incoming state is not the original state, but a Proxy object. When we change its value, immer will automatically merge the original state. As shown below:

The test code

const payload = {title:'Create Role Successfully.', status:'successful'}
const initState = {notification: {}}

describe('Notification Models:', () => {
   it('should save Notify payload:', () => {
      const saveNotify = AppModule.reducers.saveNotify
      const state = initState
      const result = saveNotify(state, { payload : {notification: payload} })
      expect(state.notification.status).toEqual('successful')
      expect(state.notification.visible).toEqual(true)}}}Copy the code

To test the effects

Although effects is not a pure function, it involves operations such as API service call, file reading and database reading, etc., but in the unit test, that is to say, in such a function, we do not need to care about the PROCESS of API call, as long as we care about whether our function initiates API requests. In subsequent logic, we need to use the result returned by the call API, so we can directly simulate a result passed to it.

Example effect function

For example, I have an effect function that makes a createInfo API request and then performs different actions based on the result of reponse. When success is true, that is, there is no error, the page is redirected and the PUT operation is performed to change the Notification state in State. The Notification message box is displayed. Of course, I’ve omitted error handling.

*createInfo({ payload: { values } }, { call, put }) {
      const { data, success } = yield call(Service.createInfo, values)
      if (data && success) {
        const { id } = data
        router.push(`/users/${id}/view`);

        const notification = {title:'Create information Successfully.', status:'successful'}
        yield put({ type: 'app/notify', payload:{notification}})
      }
}
Copy the code

Test process and principle

The effect function is actually a generator function, and many people think that writing an effect test can be done by calling.next(), but don’t really know why.

In ES6, a generator function is added. The difference between a generator function and a normal function is that it is a function that can be stopped halfway through execution. It is a good solution for asynchronous requests. Every time we encounter the yield keyword, it will pause automatically until we manually start it again. Dav encapsulates Redux-Saga, so the Effects management mechanism of Redux-Saga will start to start the function running. In the test case, we need to call.next() to start manually and continue running.

Initialization: First we need to initialize the generator function, which is not running, so nothing happens in createInfo effect.

const actionCreator = {
    type: 'info/createInfo',
    payload: {
        values: {
            name: 'abc',
            description: 'xxx',
        }
    }
}
const generator = info.effects.createInfo(actionCreator, { call, put })
Copy the code

Start execution: We call generator.next() to start the execution of the function. The function stops when the yield keyword is encountered, and the API service has not yet been invoked, but is just ready to be invoked. Calling.next() returns an object:

{ value: xxxx, done: false}
Copy the code

Value represents what yield should do next, which is the call API behavior.

let next = generator.next()
expect(next.value).toEqual(call(Service.createInfo, actionCreator.payload.values))
Copy the code

Continue to run: we call. Then again next () up and running, truly in this step function is also to execute the call (Service. CreateInfo, actionCreator. Content. Values). After receiving the result, we enter the if statement until we pause at the next yield keyword. Since calling calls returns a response, in unit testing we need to pass in a mock response when calling.next() :

next = generator.next({
        success: true,
        data: { id: '123456'}})Copy the code

At this point, the function has finished getting the ID in response and jumped to the router, and paused at the next yield keyword. At this point we can assert whether the mock router.push has been executed and whether the current value of next is put:

router.push = jest.fn()
expect(router.push).toHaveBeenCalledWith(`/list/123456/view`)
const notification = {title:'Create Information Successfully.', status:'successful'}
expect(next.value).toEqual(put({ type: 'app/notify', payload:{notification}}))
Copy the code

When we call.next() to continue, the next operation will have no yield keyword, so the function will execute until the end, and the value will be undefined:

next=generator.next()
expect(next.value).toBeUndefined()
Copy the code

The last word

I hope that through my small example, you can not only preliminarily learn the reducer and effect function test process in model of DVA framework, but also understand the implementation process of effect function and saga test method. Of course, you should also consider how to make the test more convenient and concise and reasonable in the process of writing programs, rather than just to achieve the function of the code.