【问题标题】:Async component snapshot using Jest and Redux使用 Jest 和 Redux 的异步组件快照
【发布时间】:2017-03-13 21:11:00
【问题描述】:

我发现 Jest 在测试我的 Redux React 应用程序时非常有用。但是,有很多关于如何测试异步操作创建者的示例,但我真的不知道如何对异步组件进行快照。

我想做的是类似于hovered link example from Facebook's own tutorial。他们调用一个道具函数 onMouseEnter() 并随后拍摄快照。如果 onMouseEnter() 调度使用 Redux Thunk 创建的异步操作,是否有一种简单的方法可以做到这一点?

这就是我的 thunk 的样子,它使用 axios。

  // test-api.js

  export function getLinkInfo() {
  return function(dispatch) {
    return axios.get('/api/get-link-info')
    .then(response => {
        dispatch(getLinkInfoSuccess(response.data));
        return response;
    });
  };
}

我自己的 Link 组件来了。

import React from 'react';
import { connect } from 'react-redux';
import * as api from '../../api/test-api';

class Link extends React.Component {
  render() {
    return (
      <a href='#' onMouseEnter={this.props.getLinkInfo}>
        Hover me
      </a>
      <div>{this.props.linkInfo}</div>
    );
  }
}

const mapDispatchToProps = function(dispatch) {
  return {
    getLinkInfo: function() {
      dispatch(api.getLinkInfo());
    }
  }
}

const mapStateToProps = function(store) {
  return {
    linkInfo: store.getIn(['testState', 'linkInfo'], "")
  };
};

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

最后是测试文件。

import React from 'react';
import Link from '../link';
import renderer from 'react-test-renderer';

test('Link changes linkInfo when hovered', () => {
  const component = renderer.create(
    <Link></Link>
  );
  let tree = component.toJSON();
  expect(tree).toMatchSnapshot();

  // manually trigger the callback
  tree.props.onMouseEnter();
  // re-rendering
  tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

【问题讨论】:

    标签: reactjs asynchronous redux jestjs


    【解决方案1】:

    问题是,当你想测试异步的东西时,你需要在测试中使用 Promise 的实例,要么从测试中返回它,所以 jest 知道它并可以等待它,或者在测试中使用 async await它自己(docs)

    您可以做的是在您的测试中模拟 api:

    从 'path/to/the/api' 导入 {getLinkInfo} jest.mock('path/to/the/api', () = > ({ getLinkInfo: jest.fn() }))

    这将使用具有getLinkInfo 间谍的对象覆盖模块。然后导入模块,以便您可以在测试中设置 spy 的实际实现。

    test('Link changes linkInfo when hovered', () = > {
      //create a new promise that can be returned from your test
      const p = new Promise((resolve) = > {
        //set the spy to make the request and resolve the promise
        getInfo.mockImplementation(function (dispatch) {
          return axios.get('/api/get-link-info')
            .then(response = > {
              dispatch(getLinkInfoSuccess(response.data));
              resolve(response);
            });
        };)
      };)
      const component = renderer.create(
        <Link></Link>
      );
      let tree = component.toJSON();
      expect(tree)
        .toMatchSnapshot();
      // manually trigger the callback
      tree.props.onMouseEnter();
      return p.then(() = > {
        tree = component.toJSON();
        expect(tree)
          .toMatchSnapshot()
      })
    });
    

    虽然这可能会解决您的实际问题,但我建议您不要像这样运行您的测试,调用您的真实 API,但也可以自行模拟请求。首先,您的测试会更快,并且它不依赖于某些正在运行的后端。

    关键是你想将你的 React 组件作为一个单元来测试,所以它不关心在调用 getLinkInfo 之后发生了什么。这些是getLinkInfo 的单元测试的详细信息。你的组件所知道的是,它调用getLinkInfo 传入一个回调,这个回调有时会被调用。何时调用它以及其间发生的事情不属于组件的责任。如果你考虑这样的测试,最简单的解决方案是立即调用回调。

    test('Link changes linkInfo when hovered', () = > {
      getInfo.mockImplementation(function (dispatch) {
        dispatch({
          some: 'Data'
        });
      };)
      const component = renderer.create(
        <Link></Link>
      );
      let tree = component.toJSON();
      expect(tree)
        .toMatchSnapshot();
      // manually trigger the callback
      tree.props.onMouseEnter();
      tree = component.toJSON();
      expect(tree).toMatchSnapshot()
    });
    

    【讨论】:

    • 感谢您的解决方案。我想在单个测试中针对后端和整个 Redux 流程进行测试的原因是为了替代可以在后台运行的自动浏览器 GUI 测试。所以我基本上希望在 Jest 中有一个可以等待组件重新呈现的等待方法。这真的是个坏主意吗?
    • 进行这种测试并不是一个坏主意,但是有更好的集成测试工具,例如 selenium 或 cypress.io
    【解决方案2】:

    在教程中,它们有一个有状态的组件。这需要进行这样的“体操”。

    对于一个纯粹的、无状态的组件,就像你拥有的那样,应该只测试两件事:

    1. 它可以与props 的任意组合正确呈现,
    2. 在某个事件上调用了正确的处理程序。

    但是,您只能导出 connect 生成的 HOC。您可以通过导出两者(以及mapDispatchToPropsmapStateToProps)来解决这个问题。或者,也可以通过模拟 connect 来返回测试的原始组件。

    文件如下所示:

    import …
    
    export class Link extends React.Component {
        …
    }
    
    export const mapDispatchToProps = …
    
    export const mapStateToProps = …
    
    export default connect(mapStateToProps, mapDispatchToProps)(Link);
    

    还有测试:

    import …
    import { Link, mapDispatchToProps, mapStateToProps } from './Link'
    
    test('renders correctly', () => {
      const tree = renderer.create(
        <Link linkInfo="Link info" />
      ).toJSON()
    
      expect(tree).toMatchSnapshot()
    })
    
    test('calls getLinkInfo', () => {
      const getLinkInfo = jest.fn()
    
      const tree = renderer.create(
        <Link getLinkInfo={getLinkInfo} />
      )
    
      tree.props.onMouseEnter()
    
      expect(getLinkInfo).toHaveBeenCalled()
    })
    
    test('mapDispatchToProps', () => … )
    test('mapStateToProps', () => … )
    

    这是对纯组件的完整测试。


    您问题的第二部分是关于测试异步操作创建者。棘手的部分是axios。它从何而来?我假设你在顶部导入它。所以你必须嘲笑它​​——呃,这很快就会变得一团糟。

    有一个鲜为人知的 extraArgument 你可以传递给 redux thunk。这可以作为纯依赖注入工作,这使得动作创建者非常容易测试。

    这样使用:

    const store = createStore(
      reducer,
      applyMiddleware(thunk.withExtraArgument({ axios }))
    )
    

    然后这个依赖(或者更多,如果你需要)作为第三个参数传递给 thunk:

    export function getLinkInfo() {
      return function(dispatch, getState, { axios }) {
        return axios.get('/api/get-link-info')
        .then(response => {
            dispatch(getLinkInfoSuccess(response.data));
            return response;
        });
      };
    }
    

    现在很酷。异步操作创建者的测试:

    import * as actions from './actions'
    
    describe('getLinkInfo', () => {
      const action = actions. getLinkInfo()
    
      const dispatch = jest.fn()
      const getState = () => { … }
      const axios = {
        get: jest.fn(() => Promise.resolve({
          data: {}
        }))
      }
    
      beforeEach(() => {
        deps.axios.get.mockClear()
      })
    
      test('fetches info from the server', () => {
        action(dispatch, getState, { axios })
    
        expect(axios.get).toHaveBeenCalledTimes(1)
        expect(axios.get.mock.calls).toMatchSnapshot()
      })
    
    })
    

    附:我在这里展示了这些和一些更好的 Jest 测试模式: https://github.com/robinpokorny/jest-example-hannoverjs

    【讨论】:

    • 很好的例子。目前我自己开发这个应用程序并没有太多时间,而且我觉得为组件、动作创建器和减速器编写测试会很耗时(或者至少非常 WET)。但也许我应该改用 Selenium Webdriver。对我来说重要的是进行覆盖范围广的回归测试。
    猜你喜欢
    • 1970-01-01
    • 2017-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-24
    • 2021-02-03
    • 2021-09-27
    相关资源
    最近更新 更多