【问题标题】:Testing and mocking fetch in async useEffect and async Redux-Saga在异步 useEffect 和异步 Redux-Saga 中测试和模拟 fetch
【发布时间】:2019-05-27 20:42:53
【问题描述】:

我正在测试一个使用 React-Hooks 和 Redux-Saga 的功能组件。我可以在组件的 URL 中传递参数,因为它们是登录页面组件。

我传递的 URL 是 'localhost/access/parameter',当这个参数存在时,我需要调用 async redux saga,如果 fetch 正常,我将结果放入还原商店。当结果在 redux-store 上时,我有一个 useEffect 来验证结果,如果没问题,我把她放在一个输入中。

我可以使用 axios 模拟结果,但我正在迁移为仅使用 fetch。我嘲笑 fetch,但是当我使用 mount(component),由酶提供,我不知道如何等待 redux-saga 终止请求并使用 useEffect 完成您的工作。我在 effect, saga 中放置了一个控制台日志,并记录输入道具以查看您的 value prop,但该值始终为空。我尝试使用setImmediate() 和process.nextTick()。

我用来编写代码的链接:1,2,3

我正在使用 formik,所以他们将一些道具传递给我。

我的组件

const Login = ({
  setFieldError, errors, response, fetchDomain, location, values, handleChange, handleBlur, setFieldValue, history,
}) => {

   useEffect(() => {
    async function fetchUrlDomain() {
      const { pathname } = location;
      const [, , domain] = pathname.split('/');

      if (typeof domain !== 'undefined') {
        await fetchDomain(domain);
      }
    }

    fetchUrlDomain();
  }, [fetchDomain, location]);

   useEffect(() => {
    if (typeof response === 'string') {
      setFieldError('domain', 'Domain not found');
      inputDomain.current.focus();
    } else if (Object.keys(response).length > 0) {
      setFieldValue('domain', response.Domain);
      setFieldError('domain', '');
    }
  }, [response, setFieldValue, setFieldError]);

return (
  <input name="domain" id="domain" value={values.domain} onChange={handleChange} onBlur={handleBlur} type="text" />
);
}

const LoginFormik = withFormik({
  mapPropsToValues: () => ({ domain: '' }),
  enableReinitialize: false,
  validateOnBlur: false,
  validateOnChange: false,
})(Login);

const mapStateToProps = () => ({ });

const mapDispatchToProps = dispatch => ({
  fetchDomain: (value) => {
    dispatch(action({}, constants.RESET_RESPONSE_DOMAIN));
    dispatch(action(value, constants.FETCH_DOMAIN_REQUEST));
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(LoginFormik);

我的传奇

export function* fetchDomain(action) {
  const url = yield `${mainUrl}/${action.payload}`;
  try {
    const response = yield fetch(url).then(res => res.json());
    yield put(reduxAction(response , constants.FETCH_DOMAIN_SUCCESS));
  } catch (e) {
    yield put(reduxAction(e, constants.FETCH_DOMAIN_FAILURE));
  }
}

我的减速器

case constants.FETCH_DOMAIN_FAILURE:
  return { ...initialState, response: 'Domain not found' };
case constants.FETCH_DOMAIN_SUCCESS: {
  const { payload } = action;
  return {
    ...initialState,
    id: payload.Id,
    apis: payload.Apis,
    response: payload,
  };
}
case constants.RESET_RESPONSE_DOMAIN:
  return { ...initialState };

我的测试

it('input with fetch only', (done) => {
  const mockSuccessResponse = {
    Id: 'fafafafa',
    Apis: [],
    Domain: 'NAME',
  };
  const mockJsonPromise = Promise.resolve(mockSuccessResponse);
  const mockFetchPromise = Promise.resolve({
    json: () => mockJsonPromise,
  });

  global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);

  const wrapper = mount(
    <Provider store={store}>
      <LoginForm
        history={{ push: jest.fn() }}
        location={{ pathname: 'localhost/login/Domain' }}
      />
    </Provider>,
  );

  process.nextTick(() => {
    const input = wrapper.find('#domain');
    console.log(input.props());
    expect(input.props().value.toLowerCase()).toBe('name');

    global.fetch.mockClear();
    done();
  });
});

我希望我的意见有价值,但他没有。我尝试使用jest-fetch-mock,但不起作用,我想使用原生 jest 方法,没有三十个派对库。

【问题讨论】:

    标签: jestjs fetch enzyme redux-saga react-hooks


    【解决方案1】:

    我不能说您当前的代码有什么问题。但是想提出不同的方法。

    目前您正在测试 redux 部分和组件的部分。当理想情况下你应该模拟除被测模块之外的所有内容时,它与单元测试策略相矛盾。

    所以我的意思是,如果您专注于测试组件本身,它会更容易(创建的模拟更少)并且更具可读性。为此,您还需要导出未包装的组件(在您的情况下为Login)。然后你可以只测试它的 props-vs-render 结果:

    it('calls fetchDomain() with domain part of location', () => {
        const fetchDomain = jest.fn();
        const location = { pathName: 'example.com/path/sub' }
        shallow(<Login fetchDomain={fetchDomain} location={location} />);
        expect(fetchDomain).toHaveBeenCalledTimes(1);
        expect(fetchDomain).toHaveBeenCalledWith('example.com');
    });
    
    it('re-calls fetchDomain() on each change of location prop', () => {
        const fetchDomain = jest.fn();
        const location = { pathName: 'example.com/path/sub' }
        const wrapper = shallow(<Login fetchDomain={fetchDomain} location={location} />);
        fetchDomain.mockClear();
        wrapper.setProps({ location: { pathName: 'another.org/path' } });
        expect(fetchDomain).toHaveBeenCalledTimes(1);
        expect(fetchDomain).toHaveBeenCalledWith('another.org');
    });
    

    其他情况也是如此。如果您将redux 替换为直接调用fetch() 或其他什么,或者如果您将该数据重构为来自父级而不是从redux 存储中读取,则使用这种方法查看您将不需要重写删除模拟到redux 的测试。当然,您仍然需要测试 redux 部分,但也可以单独进行。

    PS 并且 await fetchDomain(...) 在 useEffect 中没有利润,因为您不使用它返回的内容。 await 不像暂停一样工作,该代码可能会让读者感到困惑。

    【讨论】:

    • 早上好天空男孩。感谢您的帮助和回答。当我向前端编写测试时,第一步我测试组件/redux/sagas 的功能,之后,我将尝试进行完整的测试,只是模拟 API 的调用,就像集成测试一样。这种做法有错吗?关于您的回答,我会在这里尝试并发布结果,非常感谢!
    • 我不能说这是错误的方式。但是在我看来,组件 + redux + saga 的测试接近于验收测试。使用不同的工具(Selenium 或 puppeter)会更容易实现。
    • 嘿skyboyer,谢谢,你的例子帮助了我,但没有解决我的问题。 Formik 给我一个改变输入的函数,如果我在没有 formik 包装器的情况下浅化组件,输入不会改变值哈哈哈。
    • 不确定是不是这个原因。看到你同时提供value 和onChange。还有ref 没有使用,所以我看不出Formik 会如何影响它。您是否使用github.com/airbnb/enzyme/issues/2139#issuecomment-495947192 中描述的任何中间变量?
    猜你喜欢
    • 2019-11-22
    • 2021-07-04
    • 2021-04-14
    • 1970-01-01
    • 2017-12-28
    • 1970-01-01
    • 2019-12-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多