简体   繁体   中英

How to mock a function dispatching an action in React functional component using jest

I have a component that gets form data and dispatch an action with data. This action eventually makes an ajax request to the server to send that data using javascript fetch function.

import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Editor } from 'react-draft-wysiwyg';
import { EditorState } from 'draft-js';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import { getCookie } from '../../../utils/cookies';

import { postJobAction } from './redux/postJobActions';

const PostJobComponent = () => {
  const dispatch = useDispatch();
  const [editorState, setEditorState] = useState(() => EditorState.createEmpty());
  const [department, setDepartment] = useState('');

  const postJob = (event) => { // Here is happens and I am testing this function now.
    event.preventDefault();

    const jobPosterId = getCookie('id');
    const title = event.target.title.value;
    const location = event.target.location.value;
    const jobDescription = editorState.getCurrentContent().getPlainText();

    dispatch(postJobAction({
      jobPosterId,
      title,
      location,
      department,
      jobDescription,
    }));
  };

  const onDepartmentChange = (event) => {
    setDepartment(event.target.value);
  };

  return (
    <div className='post-job'>
      <form onSubmit={postJob}>
        <div>
          <label>Job Title</label>
          <input
            type='text'
            name='title'
            defaultValue=''
            className='job__title'
            placeholder='e.g. Frontend Developer, Project Manager etc.'
            required
          />
        </div>
        <div>
          <label>Job Location</label>
          <input
            type='text'
            name='location'
            defaultValue=''
            className='job__location'
            placeholder='e.g. Berlin, Germany.'
            required
          />
        </div>
        <div>
          <label>Department</label>
          <select className='job__department' required onChange={onDepartmentChange}>
            <option value=''>Select</option>
            <option value='Customer Success'>Customer Success</option>
            <option value='Professional Services'>Professional Services</option>
            <option value='Service Support'>Service And Support</option>
          </select>
        </div>
        <div style={{ border: '1px solid black', padding: '2px', minHeight: '400px' }}>
          <Editor
            required
            editorState={editorState}
            onEditorStateChange={setEditorState}
          />
        </div>
        <div>
          <button>Save</button>
        </div>
      </form>
    </div>
  );
};

export default PostJobComponent;

Here is the test in jest and enzyme for postJob function.

it('should submit job post form on save button click', () => {
        const onPostJobSubmit = jest.fn();
        const instance = wrapper.instance();
        wrapper.find('form').simulate('submit', {
      target: {
        jobPosterId: {
            value: '12312jkh3kj12h3k12h321g3',
        },
        title: {
          value: 'some value',
        },
        location: {
          value: 'some value',
        },
        department: {
            value: 'Customer',
        },
        jobDescription: {
            value: 'This is Job description.',
        },
      },
    });
        expect(onPostJobSubmit).toHaveBeenCalled();
    });

The code works perfectly fine but the tests are failing with below error.

expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

       98 |       },
       99 |     });
    > 100 |         expect(onPostJobSubmit).toHaveBeenCalled();
          |                                 ^
      101 |     });
      102 | });
      103 |

      at Object.<anonymous> (src/components/employer/jobs/postJob.test.js:100:27)

Here is the action for postJob function that dispatches action.

export const postJobAction = (payload) => {
  return {
    type: 'POST_JOB_REQUEST',
    payload,
  }
};

Here is the saga.

import { put, call } from 'redux-saga/effects';
import { postJobService } from '../services/postJobServices';

export function* postJobSaga(payload) {
  try {
    const response = yield call(postJobService, payload);
    yield [
      put({ type: 'POST_JOB_SUCCESS', response })
    ];
  } catch(error) {
    yield put({ type: 'POST_JOB_ERROR', error });
  }
}

And here is the service.

import { getCookie } from '../../../../utils/cookies';

export const postJobService = (request) => {
  return fetch('http://localhost:3000/api/v1/employer/jobs', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': getCookie('token'),
      },
      body: JSON.stringify(request.payload)
    })
    .then(response => {
      return response.json();
    })
    .then(json => {
      return json;
    })
    .catch(error => {
      return error;
    });
};

Any idea how can I fix this? I am new to testing.

By doing const onPostJobSubmit = jest.fn(); you just reacreate a variable but don't assign a new value to the onPostJobSubmit that you want to watch. To do that you will need to add a spy on the specific onPostJobSubmit function in the ./redux/postJobActions file.

To do so just add the following in your test file:

import * as actions from './redux/postJobActions';
const onPostJobSubmitSpy = jest.spyOn(actions, "postJobAction");

...and then test that this spy function as been called:

  expect(onPostJobSubmitSpy).toHaveBeenCalled();

Here is a simplified sandbox with your example:

编辑 goofy-booth-ovm4h

I think jest doesn't have the feature to mock a function inside a functional component. You can mock the useDispatch in react redux and make your necessary testing. I have given the sample code mocking useDispatch for your reference

Testing React Functional Component with Hooks using Jest

import React from "react";
import { mount } from "enzyme";

import PostJobComponent  from "./PostJobComponent ";


const mockDispatch = jest.fn();

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
useDispatch: () => mockDispatch
}));


describe("PostJob Component", () => {
  const wrapper = mount(<PostJobComponent  />);
  wrapper.instance();
  it("should submit job post form on save button click", () => {
    wrapper.find('form').simulate('submit', {
      target: {
        jobPosterId: {
            value: '12312jkh3kj12h3k12h321g3',
        },
        title: {
          value: 'some value',
        },
        location: {
          value: 'some value',
        },
        department: {
            value: 'Customer',
        },
        jobDescription: {
            value: 'This is Job description.',
        },
      },
    });
    expect(mockDispatch.mock.calls[0][0]).toEqual({ type: 'POST_JOB_REQUEST',
    payload: { title: 'some value', location: 'some value' }});
  });
});

Don't mock the useDispatch hook, use redux-mock-store to mock the store.

A mock store for testing Redux async action creators and middleware. The mock store will create an array of dispatched actions which serve as an action log for tests.

Use jest.mock() to mock postJobService function used inside the worker saga postJobSaga . So that we will not call the real API service.

When the submit event is triggered, the actions dispatched are obtained via store.getActions() . Assert whether they meet expectations.

Since postJobSaga and postJobService are asynchronous, we need to wait for the execution of the postJobService promise to complete, so a flushPromise method is used to wait for the promise micro task to complete.

It's better to test the behavior of postJob instead of the specific implementation so that our test cases will not be so fragile, because the implementation may change, but the behavior will not. For example, use the functional component of react hooks to refactor class-based component, the implementation has changed, but the behavior remains unchanged.

Eg ( I removed the irrelevant code )

index.tsx :

import React from 'react';
import { useDispatch } from 'react-redux';
import { postJobAction } from './redux/postJobActions';

const PostJobComponent = () => {
  const dispatch = useDispatch();

  const postJob = (event) => {
    event.preventDefault();
    const title = event.target.title.value;
    const location = event.target.location.value;

    dispatch(postJobAction({ title, location }));
  };

  return (
    <div className="post-job">
      <form onSubmit={postJob}></form>
    </div>
  );
};

export default PostJobComponent;

./redux/postJobActions.ts :

export const postJobAction = (payload) => {
  return {
    type: 'POST_JOB_REQUEST',
    payload,
  };
};

./redux/postJobSaga.ts :

import { put, call, takeLatest } from 'redux-saga/effects';
import { postJobService } from '../services/postJobServices';

export function* postJobSaga(payload) {
  try {
    const response = yield call(postJobService, payload);
    yield put({ type: 'POST_JOB_SUCCESS', response });
  } catch (error) {
    yield put({ type: 'POST_JOB_ERROR', error });
  }
}

export function* watchPostJobSaga() {
  yield takeLatest('POST_JOB_REQUEST', postJobSaga);
}

./services/postJobServices.ts :

export const postJobService = (request) => {
  return fetch('http://localhost:3000/api/v1/employer/jobs', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(request.payload),
  })
    .then((response) => {
      return response.json();
    })
    .then((json) => {
      return json;
    })
    .catch((error) => {
      return error;
    });
};

index.test.tsx :

import { mount } from 'enzyme';
import React from 'react';
import { Provider } from 'react-redux';
import createMockStore from 'redux-mock-store';
import createSagaMiddleware from 'redux-saga';
import { mocked } from 'ts-jest/utils';
import PostJobComponent from './';
import { watchPostJobSaga } from './redux/postJobSaga';
import { postJobService } from './services/postJobServices';

const sagaMiddleware = createSagaMiddleware();
const mws = [sagaMiddleware];
const mockStore = createMockStore(mws);

jest.mock('./services/postJobServices');

const mockedPostJobService = mocked(postJobService);

function flushPromises() {
  return new Promise((resolve) => setImmediate(resolve));
}

describe('68233094', () => {
  it('should handle form submit', async () => {
    const store = mockStore({});
    sagaMiddleware.run(watchPostJobSaga);

    mockedPostJobService.mockResolvedValueOnce({ success: true });
    const wrapper = mount(
      <Provider store={store}>
        <PostJobComponent></PostJobComponent>
      </Provider>
    );
    wrapper.find('form').simulate('submit', {
      target: {
        title: { value: 'mocked title' },
        location: { value: 'mocked location' },
      },
    });
    await flushPromises();
    const actions = store.getActions();
    expect(actions).toEqual([
      {
        type: 'POST_JOB_REQUEST',
        payload: { title: 'mocked title', location: 'mocked location' },
      },
      { type: 'POST_JOB_SUCCESS', response: { success: true } },
    ]);
  });
});

test result:

 PASS  examples/68233094/index.test.tsx (12.307 s)
  68233094
    ✓ should handle form submit (43 ms)

---------------------|---------|----------|---------|---------|-------------------
File                 | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
---------------------|---------|----------|---------|---------|-------------------
All files            |   83.33 |      100 |   55.56 |   82.14 |                   
 68233094            |     100 |      100 |     100 |     100 |                   
  index.tsx          |     100 |      100 |     100 |     100 |                   
 68233094/redux      |   91.67 |      100 |     100 |   90.91 |                   
  postJobActions.ts  |     100 |      100 |     100 |     100 |                   
  postJobSaga.ts     |   88.89 |      100 |     100 |   88.89 | 9                 
 68233094/services   |   33.33 |      100 |       0 |      20 |                   
  postJobServices.ts |   33.33 |      100 |       0 |      20 | 2-16              
---------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        14.333 s

You are not passing in the onPostJobSubmit mock function into component that renders your form. You need to be able to pass in your mock function as a prop to the component you want to test.

Here, the onSubmit of the form is hard-coded to always call postJob which is a pre-defined function in <PostJobComponent /> .

<PostJobComponent /> should be able to accept a prop for the onSubmit in order to test whether it is being called.

Change <PostJobComponent /> so it can be called as:

<PostJobComponent onSubmit={onPostJobSubmit} />

Only then will you be able to test the component using a mock function, like this:

it('should submit job post form on save button click', () => {
        const onPostJobSubmit = jest.fn();
        const wrapper = mount(<PostJobComponent onSubmit={onPostJobSubmit} />);
        const instance = wrapper.instance();
        wrapper.find('form').simulate('submit', {
      target: {
        jobPosterId: {
            value: '12312jkh3kj12h3k12h321g3',
        },
        title: {
          value: 'some value',
        },
        location: {
          value: 'some value',
        },
        department: {
            value: 'Customer',
        },
        jobDescription: {
            value: 'This is Job description.',
        },
      },
    });
        expect(onPostJobSubmit).toHaveBeenCalled();
    });

And PostJobComponent will look like this:

const PostJobComponent = ({ onSubmit }) => {
  // code you defined

  return (
    <div className='post-job'>
      <form onSubmit={onSubmit}> {/* only this line changes */}
        {/* inner divs */}
      </form>
    </div>
  );
};

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM