react jest测试_用Jest测试React和Redux应用

react jest测试

React is a popular library for building user interfaces created by Facebook. Redux on the other hand is a wonderful state management library for JavaScript apps. Put together they offer the ability to build complex React projects.

React是一个流行的库,用于构建由Facebook创建的用户界面。 另一方面, Redux是一个很棒JavaScript应用程序状态管理库。 将它们放在一起可以构建复杂的React项目。

Jest is a zero configuration JavaScript testing framework developed by Facebook. It's great for general testing but more so for testing React.

Jest是Facebook开发的零配置JavaScript测试框架。 这对常规测试非常有用,但对React来说更是如此。

Why is it important to test your applications? Poor user experiences can drive customers away from your applications.

为什么测试您的应用程序很重要? 糟糕的用户体验可能会使客户远离您的应用程序。

Comprehensive testing allows you to discover errors, inconsistencies, and vulnerabilities in your application.

全面的测试使您可以发现应用程序中的错误,不一致和漏洞。

Practicing test-driven development has been shown to save development costs and time post release. For the purposes of this article, we will simply focus on testing and not test driven development.

实践证明,测试驱动开发可以节省开发成本和发布后的时间。 出于本文的目的,我们将仅关注测试而不是测试驱动的开发。

先决条件 (Prerequisites)

You will need an understanding of how React and Redux applications work. If you are new to this, take a look at this article about the same.

您将需要了解React和Redux应用程序的工作方式。 如果您是新手,请阅读与这篇文章差不多的文章

我们将在本文中做什么 (What We'll Do in this Article)

To test JavaScript apps, a good approach to learning is to build and test! It's very hard to see JavaScript testing snippets and make sense of it unless we use it and run tests ourselves.

要测试JavaScript应用程序,学习和学习的好方法就是构建和测试! 除非我们使用它并自己运行测试,否则很难看到JavaScript测试片段并弄清它们。

  1. We'll build a to-do app

    我们将构建一个待办事项应用
  2. We'll test that to-do app

    我们将测试待办事项

入门 ( Getting Started )

We will need to install a few (jk its a lot) packages before we can get started.

在开始之前,我们将需要安装一些(很多)软件包。

yarn add --dev jest babel-jest babel-preset-es2015 babel-preset-react enzyme enzyme-to-json isomorphic-fetch moment nock prop-types react react-addons-test-utils react-dom react-redux redux redux-mock-store redux-thunk sinon

Don't forget to add a .bablerc file in your project's root folder.

不要忘记在项目的根文件夹中添加.bablerc文件。

// ./.babelrc{
  "presets": ["es2015", "react"]
}

Also add the following to your package.json

还将以下内容添加到package.json中

// ./package.json"scripts": {
    "test": "jest",
  },

To run your tests, you will execute the following command

要运行测试,您将执行以下命令

yarntest or npm test

Our file structure will look like this.

我们的文件结构将如下所示。

- **tests**/
----- **snaphots**/ // Folder containing snapshots captured from tests
--------- actions_test.js.snap
--------- async_actions_test.js.snap
--------- reducer_test.js.snap
--------- todo_component_shapshot_test.js.snap
----- actions_test.js // Action creators tests
----- async_actions_test.js // Async action creators tests
----- reducer_test.js // Reducer tests
----- todo_component_test.js // Component tests using enzyme and sinon
----- todo_component_snapshot_test.js // Component snapshot tests
- src/
----- components/
-------- Todo.js  // Component that displays To-Dos
----- redux/
-------- actions.js // app actions
-------- reducer.js // app reducer 
- node_modules/     // created by npm. holds our dependencies/packages
- .babelrc // contains babel instructions
- package.json
- .gitignore // files and folders to ignore
- README.md // Project description and instructions for running it.
- yarn.lock // dependency version lock

This is a very simplified look at what a React Redux app's folder structure would look like. However it is sufficient for us to grasp the testing concepts. Jest will by default look for test files inside of __tests__ folder. If you wish to specify your own location, you can pass the testRegex option to the Jest configuration object in your package.json. For example, if you want to place your test files in a folder named test_folders, you would write your Jest configuration as follows.

这是一个非常简化的外观,显示了React Redux应用程序的文件夹结构。 但是,我们足以掌握测试概念。 Jest默认情况下会在__tests__文件夹中查找测试文件。 如果您希望指定自己的位置,则可以将testRegex选项传递给package.json中的Jest配置对象。 例如,如果要将测试文件放置在名为test_folders的文件夹中,则可以按以下方式编写Jest配置。

// ./package.json
“jest”: {
  "testRegex": "test_folder/.*.(js|jsx)$"
},

测试您的组件 ( Testing your Components )

When you set out to test React components, there are a few primary questions you need to ask yourself.

开始测试React组件时,您需要问自己几个主要问题。

  1. What is the output of the component i.e what does it render?

    组件的输出是什么,即它呈现什么?
  2. Does the component render different results based on differing conditions?

    组件是否根据不同的条件提供不同的结果?
  3. What does the component do with functions passed to it as props?

    该组件与作为prop传递给它的函数有什么关系?
  4. What are the outcomes of a user interacting with the component?

    用户与组件进行交互的结果是什么?

Let's use the following to-do component as an example.

让我们以下面的待办事项为例。

// ./src/components/Todo.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import {connect} from 'react-redux'
import {editTodo, toggleTodo, deleteTodo} from '../redux/actions'

export class Todo extends Component {
  constructor() {
    super();
    this.state = {
      formOpen: false,
      todo: {}
    }
    this.handleOpen=this.handleOpen.bind(this);
    this.handleClose=this.handleClose.bind(this);
    this.handleFieldChange=this.handleFieldChange.bind(this);
    this.handleEdit=this.handleEdit.bind(this);
    this.handleDelete=this.handleDelete.bind(this);
    this.handleToggle=this.handleToggle.bind(this);
  }

  // Open Todo edit form
  handleOpen() {
    this.setState({formOpen: true});
  }

  // Close Todo edit form and reset any changes
  handleClose() {
    this.setState({formOpen: false});
    this.setState({todo: {}});
  }
  // Handle changes to the input fields
  handleFieldChange(e) {
    // Property to change e.g title
    var field = e.target.name;
   // New value
    var value = e.target.value;
    var todo = this.state.todo;
    var todo = Object.assign({}, todo, {[field]: value});
    this.setState({todo: todo});
  }

  handleEdit() {
    // Send to-do id and new details to actions for updating
    this.props.editTodo(this.props.id, this.state.todo);
    this.handleClose();
  }

  handleDelete() {
   // Send to-do id to actions for deletion
    this.props.deleteTodo(this.props.id);
  }

  handleToggle() {
    // Mark a to-do as completed or incomplete
    this.props.toggleTodo(this.props.id, {done: !this.props.done})
  }

  render() {
    return (
      <div className="column">
        <div className="ui brown card">
          <img className="ui image" src={this.props.url} />
          {this.state.formOpen ?
            <div className="content">
              <div className='ui form'>
                <div className='field'>
                  <label>Title</label>
                  <input type='text'
                    name="title"
                    defaultValue={this.props.title}
                    onChange={this.handleFieldChange}
                  />
                </div>
                <div className='field'>
                  <label>Project</label>
                  <input type='text'
                    name="project"
                    defaultValue={this.props.project}
                    onChange={this.handleFieldChange}
                  />
                </div>
              </div>
            </div> :
            <div className="content">
              <div className="header">{this.props.title}</div>
              <div className="meta">{this.props.project}</div>
              <div className="meta">Created {moment(this.props.createdAt).fromNow()}</div>
            </div>
          }
          <div className="extra content">
            {this.state.formOpen ?
              <div className="ui two buttons">
                <button className='ui basic green button' onClick={this.handleEdit}>
                  <i className='checkmark icon'></i> Update
                </button>
                <button className='ui basic red button' onClick={this.handleClose}>
                  <i className='remove icon'></i> Cancel
                </button>
              </div> :
              <div>
                <div className="ui toggle checkbox" style={{marginBottom: '10px'}}>
                  <input type="checkbox" name="public" value="on" defaultChecked ={this.props.done} onChange={this.handleToggle}/>
                  <label>Complete</label>
                </div>
                <div className="ui two buttons">
                  <button className='ui basic green button' onClick={this.handleOpen}>Edit</button>
                  <button className="ui basic red button" onClick={this.handleDelete}>Delete</button>
                </div>
              </div>
            }
          </div>
        </div>
      </div>
    )
  }
}

Todo.propTypes = {
  id: PropTypes.string.isRequired,
  title: PropTypes.string.isRequired,
  project: PropTypes.string.isRequired,
  done: PropTypes.bool.isRequired,
  url: PropTypes.string.isRequired,
  createdAt: PropTypes.string.isRequired,
  editTodo: PropTypes.func.isRequired,
  toggleTodo: PropTypes.func.isRequired,
  deleteTodo: PropTypes.func.isRequired
};

export default connect(null, {editTodo, toggleTodo, deleteTodo})(Todo);

You will need to add an actions file at the correct path to prevent a cannot find module error from occuring. Add the following to the file. Note that these are not proper actions they are just placeholders to prevent errors when requiring the file in our tests.

您将需要在正确的路径上添加一个动作文件,以防止发生cannot find module错误。 将以下内容添加到文件中。 请注意,这些不是正确的操作,它们只是占位符,可防止在我们的测试中需要文件时出现错误。

// ./src/redux/actions.js
export const editTodo = () => {
    console.log('Sample async function')
}

export const toggleTodo = () => {
    console.log('Sample async function')
}

export const deleteTodo = () => {
    console.log('Sample async function')
}

You will notice that since we are using Redux, we are using a higher order component(connect()) to inject Redux state in the component. The reason this is important is that when you import this component the following way;

您会注意到,由于我们使用的是Redux,因此我们使用的是更高阶的component(connect())在组件中注入Redux状态。 这很重要的原因是,当您通过以下方式导入该组件时;

import Todo from './Todo'

What you get is the wrapper component returned by connect(), and not the App component itself. Ideally, you are interested in testing the rendering of the component without the Redux store. If you want to test the connected component, you can wrap it in a <Provider> with a store created specifically for this unit test. However, the only reason you would need to do this is to check if your component reacts properly to changes in the store. As React Redux takes care of its own tests, you don't need to do this. You can mock the props as they would appear under different conditions after connecting the component. To assert, manipulate, and traverse your React Components' output we will use Enzyme a JavaScript Testing utility for React by Airbnb. We'll use Enzyme's shallow method to render our component. Shallow is preferred because it does not render the children of the component whose behavior we do not want to assert. Let's look at what the set up for our tests will look like.

您得到的是connect()返回的包装器组件,而不是App组件本身。 理想情况下,您有兴趣在没有Redux存储的情况下测试组件的呈现。 如果要测试连接的组件,则可以使用专门为此单元测试创​​建的存储库将其包装在<Provider> 。 但是,您唯一需要这样做的原因是检查组件是否对存储中的更改做出正确的React。 由于React Redux负责自己的测试,因此您不需要这样做。 您可以在连接组件后模拟道具在不同条件下的显示。 为了声明,操作和遍历您的React组件的输出,我们将使用Airbnb的React Enzyme JavaScript测试实用程序。 我们将使用酶的浅层方法渲染组件。 首选Shallow,因为它不会呈现我们不想断言其行为的组件的子代。 让我们看一下测试设置。

// ./**tests**/todo_component_test.js
import React from 'react';
import { shallow } from 'enzyme';
import {Todo} from '../src/components/Todo';
import sinon from 'sinon';

//Use array destructurig to create mock functions.
let [editTodo, toggleTodo, deleteTodo] = new Array(3).fill(jest.fn());

function shallowSetup() {
  // Sample props to pass to our shallow render
  const props = {
    id: "7ae5bfa3-f0d4-4fd3-8a9b-61676d67a3c8",
    title: "Todo",
    project: "Project",
    done: false,
    url: "https://www.photos.com/a_photo",
    createdAt: "2017-03-02T23:04:38.003Z",
    editTodo: editTodo,
    toggleTodo: toggleTodo,
    deleteTodo: deleteTodo
  }
  // wrapper instance around rendered output
  const enzymeWrapper = shallow(<Todo {...props} />);

  return {
    props,
    enzymeWrapper
  };
}

What does this setup do? We define sample props to pass to the component that we are rendering. We have written a function that is available anywhere in our tests that creates an Enzyme wrapper. To answer our first and second questions that we mentioned above, the component renders a card showing the details of a to-do by default and a form to edit the to-do when the edit button is clicked.

此设置有什么作用? 我们定义示例道具以传递到我们正在渲染的组件。 我们编写了一个函数,该函数可在我们的测试中的任何位置使用,该函数可创建一个酶包装器。 为了回答上面提到的第一个和第二个问题,该组件将渲染一张卡片,显示默认情况下的待办事项以及单击编辑按钮时可编辑待办事项的表格。

We can assert the unique rendered elements of the card as follows:

我们可以声明卡的唯一呈现元素,如下所示:

// ./**tests**/todo_component_test.js
describe('Shallow rendered Todo Card', () => {
  it('should render a card with the details of the Todo', () => {
    // Setup wrapper and assign props.
    const { enzymeWrapper, props } = shallowSetup();
    // enzymeWrapper.find(selector) : Find every node in the render tree that matches the provided selector. 
    expect(enzymeWrapper.find('img').hasClass('ui image')).toBe(true);
    expect(enzymeWrapper.find('.header').text()).toBe(props.title);
    expect(enzymeWrapper.find('button.ui.basic.red.button').text()).toBe('Delete');
    // enzymeWrapper.containsMatchingElement(node i.e reactElement) : Check if the provided React element matches one element in the render tree. Returns a boolean.
    expect(enzymeWrapper.containsMatchingElement(<button>Delete</button>)).toBe(true);
  });
});

We have used Enzyme's find to look for elements that are rendered only when the to-do card is visible. In addition, we can assert against the properties of these elements e.g that they have a certain class or contain certain text. You can take a look at Enzyme's documentation to see all the matchers you can use to test your component. Go ahead and run npm test at the root of your folder to run your tests. Next, we need to assert that the elements that we expect to be present when the form is open are indeed present. For us to do this, we need to simulate a click on the edit button. We can do this as follows

我们使用了酶的发现来查找仅在待办事项卡可见时才呈现的元素。 另外,我们可以针对这些元素的属性进行断言,例如,它们具有特定的类或包含特定的文本。 您可以查看Enzyme的文档,以查看可用于测试组件的所有匹配器。 继续并在文件夹的根目录下运行npm test以运行测试。 接下来,我们需要断言在表单打开时我们期望存在的元素确实存在。 为此,我们需要模拟点击“编辑”按钮。 我们可以这样做

const button = wrapper.find('button').first();
button.simulate('click');

We'll need to confirm that the state has changed to reflect that the form is open, input boxes are present and their default values are the original to-do properties and finally, that new buttons with different text are present. This will look like this:

我们需要确认状态已更改,以反映表单已打开,存在输入框,并且它们的默认值是原始的待办事项属性,最后是存在带有不同文本的新按钮。 看起来像这样:

// ./**tests**/todo_component_test.js
describe('Todo form', () => {
  let wrapper, props_;
  beforeEach(() => {
    // spy on the component handleOpen method
    sinon.spy(Todo.prototype, "handleOpen");
    const { enzymeWrapper, props } = shallowSetup();
    wrapper = enzymeWrapper;
    props_ = props;
  });
  afterEach(() => {
    Todo.prototype.handleOpen.restore();
  });
  it('should update the state property _**`formOpen`**_ and call handleOpen when edit button is clicked', () => {
    // find the edit button and simulate a click on it
    const button = wrapper.find('button').first();
    button.simulate('click');
    // The handleOpen method should be called.
    expect(Todo.prototype.handleOpen.calledOnce).toBe(true);
    // The value of this.state.formOpen should now be true
    expect(wrapper.state().formOpen).toEqual(true);
  });
  it('should display different buttons', () => {
    const button = wrapper.find('button').first();
    button.simulate('click');
    // When we click the edit button, the Update button should be present.
    expect(wrapper.find('button.ui').length).toBe(2);
    expect(wrapper.find('button.ui.basic.green.button').text()).toBe(' Update');
  });
  it('should display current values in edit fields', () =>{
    const button = wrapper.find('button').first();
    button.simulate('click');
    // Before any edits are made, the prepopulated values in the input fields should be the same passed through props.
    expect(wrapper.find('input').at(0).props().defaultValue).toEqual(props_.title);
  });
});

We are resetting the wrapper before each test using a beforeEach to assign a fresh render. You may also notice that we asserted that a component method was called. Using Sinon, we can spy on component methods to confirm that they were called and what arguments they were called with. A test spy is a function that records arguments, return value, and exceptions thrown for all its calls. sinon.spy(object, "method") creates a spy that wraps the existing function object.method. Here, we are using a spy to wrap an existing method. Unlike spies created as anonymous functions, the wrapped method will behave as normal but we will have access to data about all calls. This helps to ascertain that our component methods are working as expected. An important point to note is that we spy on the component prototype before it's shallow mounted otherwise it will be bound to the original and we'll be unable to assert whether it was called. After each test, we restore the original method so we can have a fresh spy so data from previous tests does not skew results for subsequent tests. Run npm test again to ensure your tests are passing. We could have directly manipulated the state to open the form like this:

在每次测试之前,我们都会使用beforeEach分配新鲜的渲染器来重置包装器。 您可能还会注意到,我们断言已调用了组件方法。 使用Sinon,我们可以监视组件方法以确认它们已被调用以及使用了哪些参数。 测试间谍是一个函数,它记录自变量,返回值和为其所有调用引发的异常。 sinon.spy(object, "method")创建一个间谍,该间谍包装了现有的函数object.method 。 在这里,我们使用间谍来包装现有方法。 与作为匿名函数创建的间谍不同,包装的方法将正常运行,但是我们将可以访问有关所有调用的数据。 这有助于确定我们的组件方法是否按预期工作。 需要注意的重要一点是,我们在将组件原型浅装之前先对其进行监视,否则它将被绑定到原始原型上,因此我们将无法断言是否调用了该原型。 每次测试后,我们都将恢复原始方法,这样我们就可以拥有一个新的间谍,因此先前测试中的数据不会偏斜后续测试的结果。 再次运行npm test以确保测试通过。 我们可以直接操纵状态来打开表单,如下所示:

wrapper.setState({ formOpen: true });

However, the approach we took allows us to also test user actions. Next, with the form open we can change some values and submit them to see what happens.

但是,我们采用的方法还可以测试用户的操作。 接下来,打开表单,我们可以更改一些值并将其提交以查看会发生什么。

// ./**tests**/todo_component_test.js
describe('Editing todos', () => {
  let wrapper, props_;
  //In this before each, we are opening the form and changing the to-do title value before each of the tests is run. This helps us to avoid having to do this repeatedly for every it block.
  beforeEach(() => {
    // spy on the component handleFieldChange method
    sinon.spy(Todo.prototype, "handleFieldChange");
    // spy on the component handleEdit method
    sinon.spy(Todo.prototype, "handleEdit");
    // spy on the component handleClose method
    sinon.spy(Todo.prototype, "handleClose");
    const { enzymeWrapper, props } = shallowSetup();
    wrapper = enzymeWrapper;
    props_ = props;
    const button = wrapper.find('button').first();
    button.simulate('click');
    // find the input field containing the todo title and simulate a change to it's value
    const titleInput = wrapper.find('input').at(0);
    titleInput.simulate('change', {
      target: {
        value: 'Changed title',
        name: 'title'
      },
    });
  });
  afterEach(() => {
    Todo.prototype.handleFieldChange.restore();
    Todo.prototype.handleEdit.restore();
    Todo.prototype.handleClose.restore();
  });
  it('should change state when input values change and call handleFieldChange', () => {
    // this.state.todo should now have a title field with it's value as the new title we entered.
    expect(wrapper.state().todo.title).toEqual('Changed title');
    // Since we simulated a change to an input field, the handleFieldChange event handler should be called.
    expect(Todo.prototype.handleFieldChange.calledOnce).toBe(true);
  });
  describe('Submit edits', () => {
    it('should call handleEdit, editTodo and handleClose when update button is clicked', () => {
      const button = wrapper.find('button.ui.basic.green.button');
      button.simulate('click');
      // Confirm that the different component methods called when we submit edits are called.
      expect(Todo.prototype.handleEdit.calledOnce).toBe(true);
      expect(Todo.prototype.handleClose.calledOnce).toBe(true);
      // the mock function we passed to the renderer instead of the action should be called and with the new values we entered.
      expect(editTodo).toBeCalledWith(props_.id, {"title": "Changed title"});
    });
  });
});

We'll target the different inputs and change the data by simulating changes. Since we are spying on handleFieldChange event handler we expect it to be called. If the changes to the inputs are successful, we expect these values to reflect in state so we cross check that too. Finally, when we submit the new values, we expect that the editTodo function passed as a prop to the component is called and with the new values. You can use what we have done above to test all the different functions passed as props, component methods as well as the component itself. Execute the test command again to check on the status of your new tests.

我们将针对不同的输入,并通过模拟更改来更改数据。 由于我们正在监视handleFieldChange事件处理程序,因此我们希望它被调用。 如果输入更改成功,我们希望这些值能够反映在状态中,因此我们也要进行交叉检查。 最后,当我们提交新值时,我们期望调用并作为新属性传递给组件的editTodo函数。 您可以使用上面所做的测试来测试作为道具传递的所有不同功能,组件方法以及组件本身。 再次执行test命令以检查新测试的状态。

Another way to test components is using snapshots. Snapshots enable you to identify unexpected changes in the UI. The first time you run the test, it creates a reference image which it compares to on subsequent runs. If the new image does not match with the old one, the test fails. Therefore, either an unexpected change occurred or the snapshot needs to be updated. You will be prompted to update the snapshot or make changes and run the test again.

测试组件的另一种方法是使用快照。 快照使您能够识别UI中的意外更改。 第一次运行测试时,它会创建一个参考图像,并与后续运行中的图像进行比较。 如果新图像与旧图像不匹配,则测试失败。 因此,发生意外更改或需要更新快照。 系统将提示您更新快照或进行更改,然后再次运行测试。

You can run Jest with a flag that tells it to regenerate snapshots. However, I prefer to confirm the changes are intentional before updating snapshots. Let's take a look at how we would test our component above using snapshots. First, we'll test the default to-do details view.

您可以使用指示其重新生成快照的标志来运行Jest。 但是,我更喜欢在更新快照之前确认更改是有意的。 让我们看一下如何使用快照测试上面的组件。 首先,我们将测试默认的待办事项详细信息视图。

// ./**tests**/todo_component_snaphot_test.js
import React from 'react';
import toJson from 'enzyme-to-json';
import moment from 'moment';
import { shallow } from 'enzyme';
import {Todo} from '../src/components/Todo';

it('Renders correctly', () => {
  const wrapper = shallow(
    <Todo
      id = '1'
      title = 'Todo'
      project = 'Project'
      done = {false}
      url =  "https://www.photos.com/a_photo"
      createdAt = {moment().subtract(1, 'days').format()}
      editTodo = {jest.fn()}
      toggleTodo = {jest.fn()}
      deleteTodo = {jest.fn()}
    />
  );
  expect(toJson(wrapper)).toMatchSnapshot();
});

We render the component and use enzyme-to-json to convert the Enzyme wrapper to a format compatible with Jest snapshot testing. You will also notice that we are no longer passing a static createdAt prop to the component. This is because we are using moment's fromNow function in the component which converts the date supplied to relative time. Therefore, we need to ensure that the time displayed is the same. So we create a moment object for the day before the test is run. This means the created at date in the to-do details view will always be a day ago. Run your tests and ensure that the results are as anticipated.

我们渲染该组件,并使用enzyme-to-json将酶包装程序转换为与Jest快照测试兼容的格式。 您还将注意到,我们不再将静态的createdAt属性传递给组件。 这是因为我们在组件中使用了力矩的fromNow函数,该函数将提供的日期转换为相对时间。 因此,我们需要确保显示的时间相同。 因此,我们在测试运行前的一天创建一个矩对象。 这意味着待办事项详细信息视图中的创建日期将始终是a day ago 。 运行测试,并确保结果符合预期。

If there are any actions taken by the user that cause the rendered view to look different, you can simulate those actions and take a snapshot of the rendered component.

如果用户采取了任何措施使渲染视图看起来有所不同,则可以模拟这些操作并拍摄渲染组件的快照。

测试动作创建者和异步动作创建者 ( Testing your action creators and async action creators )

Action creators are functions which return plain objects. When testing action creators we assert that the correct action creator was called and the right action was returned. We can test actions on their own but I prefer to test their interaction with the store. We'll use redux-mock-store, a mock store for testing your Redux async action creators and middleware. The mock store creates an array of dispatched actions which work as an action log for tests. We can do this as follows

动作创建者是返回简单对象的函数。 在测试动作创建者时,我们断言已调用了正确的动作创建者并返回了正确的动作。 我们可以自己测试动作,但我更喜欢测试他们与商店的互动。 我们将使用redux-mock-store (一个模拟商店)来测试您的Redux异步操作创建者和中间件。 模拟存储区创建了一组分派的动作,这些动作用作测试的动作日志。 我们可以这样做

// ./**tests**/actions_test.js
import configureMockStore from 'redux-mock-store'

// In a real application we would import this from our actions
const createSuccess = (todo) => ({
  type: 'CREATE_SUCCESS',
  todo
});

// Create a mock store
const mockStore = configureMockStore()
const store = mockStore({})
describe('action creators', () => {
  it('creates CREATE_SUCCESS when creating a to-do was successful', () => {
    // Dispatch the createSuccess action with the values of a new to-do.
    store.dispatch(createSuccess(
      {
        "id":1,
        "title":"Example",
        "project":"Testing",
        "createdAt":"2017-03-02T23:04:38.003Z",
        "modifiedAt":"2017-03-22T16:44:29.034Z"
      }
    ));
    expect(store.getActions()).toMatchSnapshot();
  });
});

For the above action, calling store.getActions will return this:

对于上述操作,调用store.getActions将返回以下内容:

[
  {
    "type":"CREATE_SUCCESS",
    "todo": {
      "id":1,
      "title":"Example",
      "project":"Testing",
      "createdAt":"2017-03-02T23:04:38.003Z",
      "modifiedAt":"2017-03-22T16:44:29.034Z"
    }
  }
]

To avoid having to type this all and having to make changes to expected actions every time we change our code, we can use snapshots instead of typing out the data again. Now, let's look at async action creators. For async action creators using Redux Thunk or other middleware, we'll completely mock the Redux store and use Nock to mock the HTTP requests.

为了避免每次都要键入所有代码并在每次更改代码时都必须对预期的操作进行更改,我们可以使用快照而不是再次键入数据。 现在,让我们看一下异步操作的创建者。 对于使用Redux Thunk或其他中间件的异步动作创建者,我们将完全模拟Redux存储,并使用Nock模拟HTTP请求。

Example

// ./**tests**/async_actions_test.js
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'
import nock from 'nock'
import fetch from 'isomorphic-fetch'

const middlewares = [ thunk ]
const mockStore = configureMockStore(middlewares)

// We would import these from actions in real case testing.
const success = (todos) => ({
  type: 'FETCH_TODO_SUCCESS',
  todos
});

// Async action creator to fetch to-dos from an API
const fetchTodos = () => (dispatch) => {
 return fetch('https://localhost:8000/api/todos')
  .then(response => {
    return response.json()
  })
  .then(json => {
    dispatch(success(json));
  })
}

describe('async actions', () => {
  let store;
  let fetchTodosData = [
    {
      "id":1,
      "title":"Example",
      "project":"Testing",
      "createdAt":"2017-03-02T23:04:38.003Z",
      "modifiedAt":"2017-03-22T16:44:29.034Z"
    }
  ];
  beforeEach(() => {
    store = mockStore({});
  });
  afterEach(() => {
    // clear all HTTP mocks after each test
    nock.cleanAll();
  });

  it('creates FETCH_TODO_SUCCESS when fetching to-dos has been done', () => {
    // Simulate a successful response
    nock('https://localhost:8000')
      .get('/api/todos') // Route to catch and mock
      .reply(200, fetchTodosData); // Mock reponse code and data

    // Dispatch action to fetch to-dos
    return store.dispatch(fetchTodos())
      .then(() => { // return of async actions
        expect(store.getActions()).toMatchSnapshot();
      })
  })
});

When we call the fetchTodos async action creator, it makes a request to an API to get to-dos. Once we have received the response it then dispatches the FETCH_TODO_SUCCESS action with the array of to-dos received from the API. We have only tested an action that sends out a GET request. When you are testing POST request, it is very important that the options passed are the same as the request body in your async action creator. I find the easiest way to do this is to stringify the sample request body using JSON.stringify then supply it to Nock. Don't forget to run your newly added tests before proceeding.

当我们调用fetchTodos异步操作创建者时,它会向API发出请求以获取待办事项。 一旦我们收到响应,它就会使用从API接收的待办事项数组调度FETCH_TODO_SUCCESS操作。 我们仅测试了发出GET请求的操作。 在测试POST请求时,传递的选项与异步操作创建者中的请求正文相同是非常重要的。 我发现最简单的方法是使用JSON.stringify将示例请求主体字符串化,然后将其提供给Nock。 在继续操作之前,请不要忘记运行新添加的测试。

测试您的减速器 ( Testing your reducers )

Reducers return a new state after applying actions to the previous state. Sample reducer

在对先前状态执行操作后,Reducer返回新状态。 Sample reducer

// ./src/redux/reducer.js
export const initialState = { // Exporting it for test purposes
  requesting: false,
  todos: [],
  error: null,
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case 'TODO_REQUEST':
      // Changes the requesting field in state to true to show we are currently fetching to-dos
      return Object.assign({}, state,
        {
          requesting: true,
          error: null,
        }
        // State should look like this:
        // {
        //  requesting: true,
        //  todos: [],
        //  error: null,
        // }
      );
    default:
      return state;
  }
}

When testing a reducer, you pass it the initial state and then the action created. The output returned should match the new state. Looking at the reducer above, we can see that in the initial state, the requesting field is set to false. When an action whose type is TODO_REQUEST is dispatched, the field is changed to true. In our test, we'll dispatch this action and confirm that the returned state has changed accordingly. We will be taking the same approach of matching the output to snapshots as we've done before. A test of the reducer defined above would look like this:

测试减速器时,您将其传递给初始状态,然后再创建操作。 返回的输出应与新状态匹配。 查看上面的reducer,我们可以看到在初始状态下,请求字段设置为false 。 调度类型为TODO_REQUEST的操作时,该字段将更改为true 。 在我们的测试中,我们将分派此操作并确认返回的状态已相应更改。 我们将采用与之前相同的方法将输出与快照进行匹配。 上面定义的reducer的测试看起来像这样:

// ./**tests**/reducer_test.js
import reducer from '../src/redux/reducer'
import {initialState} from '../src/redux/reducer'

describe('todos reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toMatchSnapshot()
  })

  it('should handle TODO_REQUEST', () => {
    expect(
      reducer(initialState,
      {
        type: 'TODO_REQUEST'
      })
    ).toMatchSnapshot()
  })
})

Finally, run npm test to ascertain your tests are passing.

最后,运行npm test以确定您的测试通过了。

覆盖范围 ( Coverage )

Jest provides a very simple way to generate coverage. To do this, run:

笑话提供了一种非常简单的方法来生成覆盖率。 为此,请运行:

npm test -- --coverage

This will produce a coverage folder in your root directory with all the coverage information.

这将在您的根目录中生成一个coverage文件夹,其中包含所有coverage信息。

结论 ( Conclusion )

Jest makes it very easy to test React applications. In addition, by leveraging Enzyme's API, we are able to easily traverse components and test them. We have barely scratched the surface of what these packages have to offer. There is a lot more you can do with them and as a result, you will always be one step ahead of nasty surprises. If you want to see a complete React Redux running app with fully featured tests, you can take a look here. So go on and try Jest today if you haven't.

Jest使测试React应用程序变得非常容易。 此外,通过利用酶的API,我们能够轻松遍历组件并进行测试。 我们几乎没有涉及这些软件包所提供的内容。 您可以使用它们做更多的事情,因此,您始终比令人讨厌的惊喜领先一步。 如果您想查看具有功能齐全的测试的完整的React Redux运行应用程序,可以在这里查看。 因此,如果没有,请继续尝试今天的Jest。

翻译自: https://scotch.io/tutorials/testing-react-and-redux-apps-with-jest

react jest测试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值