【问题标题】:How to mock a function dispatching an action in React functional component using jest如何使用 jest 模拟在 React 功能组件中调度动作的函数
【发布时间】:2021-09-14 20:59:47
【问题描述】:

我有一个组件,它获取表单数据并用数据发送一个动作。此操作最终向服务器发出 ajax 请求,以使用 javascript fetch 函数发送该数据。

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;

这里是postJob功能的玩笑和酶测试。

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();
    });

代码运行良好,但测试失败并出现以下错误。

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)

这里是 postJob 函数的动作,它调度动作。

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

这是传奇。

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 });
  }
}

这里是服务。

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;
    });
};

知道如何解决这个问题吗?我是测试新手。

【问题讨论】:

    标签: javascript reactjs jestjs enzyme redux-saga


    【解决方案1】:

    您没有将 onPostJobSubmit 模拟函数传递给呈现表单的组件。您需要能够将模拟函数作为道具传递给您要测试的组件。

    这里,表单的onSubmit 被硬编码为始终调用postJob,这是&lt;PostJobComponent /&gt; 中的预定义函数。

    &lt;PostJobComponent /&gt; 应该能够接受 onSubmit 的道具,以便测试它是否被调用。

    更改&lt;PostJobComponent /&gt;,使其可以称为:

    <PostJobComponent onSubmit={onPostJobSubmit} />
    

    只有这样您才能使用模拟函数测试组件,如下所示:

    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();
        });
    

    PostJobComponent 将如下所示:

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

    【讨论】:

    • 问题是PostJobComponent 没有父组件。这意味着我不能将函数作为道具传递给这个组件。 PostJobComponent 本身就是一个父组件。在这种情况下,我该如何为此编写测试?
    • 我还用action creatorsagaservice更新了问题。
    【解决方案2】:

    通过执行const onPostJobSubmit = jest.fn();,您只需重新创建一个变量,但不要为您要查看的onPostJobSubmit 分配新值。为此,您需要在 ./redux/postJobActions 文件中的特定 onPostJobSubmit 函数上添加一个间谍。

    为此,只需在您的测试文件中添加以下内容:

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

    ...然后测试这个间谍函数是否被调用:

      expect(onPostJobSubmitSpy).toHaveBeenCalled();
    

    这是一个简化的沙箱,其中包含您的示例:

    【讨论】:

    • 即使您没有进行更改,也会调用动作调度 postJobAction。问题是当我在PostJobComponent 内部postJob 方法console.log('ID: ', jobPosterId) // EMPTY; console.log('TITLE: ', title); // some value console.log('LOCATION: ', location); // some value console.log('DESC: ', jobDescription); // EMPTY console.log('DEPT: ', department); // Customer Success 内部进行console.log 时。所以 jobPosterId 和 jobDescription 是空的。我想这可能是问题所在。但为什么它们是空的?
    • 嗯,不,它被 submit 模拟调用并从模拟对象中获取正确的值。 jobDescriptiondepartment 的值为空是另一个问题,但这里所需的测试正在运行。
    • 现在,当我在测试中使用expect(onPostJobSubmitSpy).toHaveBeenCalledWith({ jobPosterId: '12312jkh3kj12h3k12h321g3', title: 'some value', location: 'some value', jobDescription: 'This is Job description.', }); 时,这正是由于我的第一条评论而失败的原因。它需要 jobPosterIdjobDescription 的值,但它正在接收空字符串。
    • 在您的代码中,您通过使用editorState.getCurrentContent().getPlainText() 获得了jobDescription 值,而不像另一个使用event.target 参数事件。因此,将jobDescription 添加到提交模拟对象不会改变任何内容,您需要在测试中在文本编辑器中添加文本或模拟getPlainText() 函数。然而,这部分与最初的问题无关,所以我建议打开另一个问题,因为 Expected number of calls: &gt;= 1 Received number of calls: 0 问题已经解决,如果这对你来说没问题。
    【解决方案3】:

    我认为 jest 没有在函数式​​组件中模拟函数的特性。您可以在 react redux 中模拟 useDispatch 并进行必要的测试。我已经给出了模拟 useDispatch 的示例代码供您参考

    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' }});
      });
    });
    

    【讨论】:

    • 这个测试应该显示所有你期望的值和你得到的值。您可以根据预期值更新 toEqual。 - 预期 + 接收到的对象 { "payload": Object { "location": "some value", - "title": "some value", + "title": "some value", }, "type": "POST_JOB_REQUEST" , }
    【解决方案4】:

    不要模拟 useDispatch 钩子,使用 redux-mock-store 模拟商店。

    用于测试 Redux 异步操作创建者和中间件的模拟商店。模拟存储将创建一个调度的动作数组,作为测试的动作日志。

    使用jest.mock() 模拟在worker saga postJobSaga 中使用的postJobService 函数。这样我们就不会调用真正的API服务了。

    提交事件触发后,通过store.getActions()获取派发的动作。断言它们是否符合预期。

    由于postJobSagapostJobService是异步的,我们需要等待postJobServicepromise的执行完成,所以使用flushPromise方法等待promise微任务完成。

    最好测试postJob的行为而不是具体的实现,这样我们的测试用例就不会那么脆弱,因为实现可能会改变,但行为不会。例如,使用 react hooks 的功能组件重构基于类的组件,实现发生了变化,但行为保持不变。

    例如(我删除了不相关的代码

    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 } },
        ]);
      });
    });
    

    测试结果:

     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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-08-09
      • 1970-01-01
      • 2019-07-22
      • 1970-01-01
      • 1970-01-01
      • 2020-03-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多