如何使用Jest和Enzyme进行React应用的持续集成

226 阅读17分钟

本教程包括:

  1. 克隆并设置一个React和Redux的样本应用
  2. 为该应用编写测试
  3. 配置持续集成以实现自动化测试

React仍然是许多UI开发者的首选Web框架,在2021年Stack Overflow开发者调查中超过了jQuery,成为最受欢迎的框架。它为构建数据驱动的用户界面提供了一个直观的模型,并在这些数据发生变化时有效地更新DOM。React与Redux很好地配对,管理React渲染界面所需的数据。Redux提供了一种可预测的方式来结构和更新这些前端应用程序中的数据。

在本教程中,我将带领你设置一个React和Redux应用的样本,并为其编写测试。我将向你展示如何用CircleCI配置持续集成,以实现自动化测试,并确保你添加的任何新代码不会破坏现有功能。

前提条件

要跟上本教程,需要一些东西。

  1. 在你的机器上安装Nodev12.22.12(lts/erbium)。
  2. ReactRedux有基本的了解
  3. 一个CircleCI账户
  4. 一个GitHub账户

我们的教程是与平台无关的,但使用CircleCI作为一个例子。如果你没有CircleCI账户,请**在这里注册一个免费账户。**

开始使用

这个项目建立在Redux Async实例应用的基础上。使用这个应用程序将显示你如何将测试添加到一个真正的应用程序,这应该有助于你构建自己的应用程序。

在添加测试之前,你需要找出该应用的功能。最好的方法是克隆该应用程序并在本地运行它。要克隆应用程序并安装其依赖项,请运行。

git clone --depth 1 --branch template https://github.com/CIRCLECI-GWP/react-jest-enzyme.git
npm install
npm start

Local NPM start - terminal

用Git和GitHub跟踪进度

在开发应用程序的过程中,你需要用Git来跟踪变化。删除.git 目录,然后运行git init

从克隆的 repo 的根目录,运行。

rm -rf .git
git init

这将创建一个新的 git 仓库。参考创建一个仓库来学习如何在GitHub上推送和跟踪你的变化。

CI-Jest-Enzyme

实例应用功能的演练

该示例应用程序旨在通过从Reddit API中获取数据,显示所选子红点的当前头条。用户可以选择他们想看的头条新闻的子版块。头条新闻被加载并显示在屏幕上。用户还可以通过点击刷新按钮来更新当前所选子红点的数据。

这个简单的应用程序是一个很好的例子,因为它有大多数现实世界的应用程序所需要的所有组件,包括。

  • 从API中获取数据
  • 用户互动
  • 同步和异步的行动
  • 呈现和容器组件

现在你应该已经了解了这个例子的作用,你应该在自己的GitHub账户上有一个副本。你的下一步是添加测试。

测试React组件

如果你检查package.json 文件,你会发现你已经配置了一个test 命令。

 "test": "react-scripts test"

react-scripts jest 已经安装并配置好了,所以你不需要再安装它。但你确实需要安装 ,以及它对你的React版本的适配器。enzyme

npm install --save-dev enzyme enzyme-adapter-react-16

接下来,你需要配置enzyme 来使用适配器。react-scripts 支持在一个src/setupTests.js 文件中配置测试工具。

创建该文件并输入。

import Enzyme from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

我建议使用快照测试来跟踪组件的变化。在这种方法中,你对组件进行快照,当组件的渲染输出发生变化时,你可以很容易地发现所做的改变。快照也是可读的,所以这是一种更容易验证组件是否呈现预期输出的方法。

要使用快照技术,你需要安装enzyme-to-json 包,以便在测试期间将React组件转换为快照。

npm install --save-dev enzyme-to-json

你还需要配置jest 来使用这个包作为快照序列化器。在package.json 中通过添加配置来实现。

"jest": {
    "snapshotSerializers": [
      "enzyme-to-json/serializer"
    ]
  }

现在你已经准备好开始测试了。

组件测试

开始为App 组件编写测试。一个好的起点是添加一个快照测试,以确保在所需的道具下,该组件能渲染出预期的输出。

首先,你需要导入App组件,但在App.js ,唯一导出的是组件的Redux-connected版本:export default connect(mapStateToProps)(App) 。你想测试组件的渲染,而不是它与redux 的交互,所以你需要同时导出底层的App 组件。通过添加这个片段到App.js 来完成。

export { App };

回顾一下,这就是src/containers/App.js 文件应该包含的内容。

import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import {
  selectSubreddit,
  fetchPostsIfNeeded,
  invalidateSubreddit,
} from "../actions";
import Picker from "../components/Picker";
import Posts from "../components/Posts";

class App extends Component {
  static propTypes = {
    selectedSubreddit: PropTypes.string.isRequired,
    posts: PropTypes.array.isRequired,
    isFetching: PropTypes.bool.isRequired,
    lastUpdated: PropTypes.number,
    dispatch: PropTypes.func.isRequired,
  };
  componentDidMount() {
    const { dispatch, selectedSubreddit } = this.props;
    dispatch(fetchPostsIfNeeded(selectedSubreddit));
  }
  componentWillReceiveProps(nextProps) {
    if (nextProps.selectedSubreddit !== this.props.selectedSubreddit) {
      const { dispatch, selectedSubreddit } = nextProps;
      dispatch(fetchPostsIfNeeded(selectedSubreddit));
    }
  }
  handleChange = (nextSubreddit) => {
    this.props.dispatch(selectSubreddit(nextSubreddit));
  };
  handleRefreshClick = (e) => {
    e.preventDefault();
    const { dispatch, selectedSubreddit } = this.props;
    dispatch(invalidateSubreddit(selectedSubreddit));
    dispatch(fetchPostsIfNeeded(selectedSubreddit));
  };
  render() {
    const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props;
    const isEmpty = posts.length === 0;
    return (
      <div>
        <Picker
          value={selectedSubreddit}
          onChange={this.handleChange}
          options={["reactjs", "frontend"]}
        />
        <p>
          {lastUpdated && (
            <span>
              Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{" "}
            </span>
          )}
          {!isFetching && (
            <button onClick={this.handleRefreshClick}>Refresh</button>
          )}
        </p>
        {isEmpty ? (
          isFetching ? (
            <h2>Loading...</h2>
          ) : (
            <h2>Empty.</h2>
          )
        ) : (
          <div style={{ opacity: isFetching ? 0.5 : 1 }}>
            <Posts posts={posts} />
          </div>
        )}
      </div>
    );
  }
}

const mapStateToProps = (state) => {
  const { selectedSubreddit, postsBySubreddit } = state;
  const {
    isFetching,
    lastUpdated,
    items: posts,
  } = postsBySubreddit[selectedSubreddit] || {
    isFetching: true,
    items: [],
  };
  return {
    selectedSubreddit,
    posts,
    isFetching,
    lastUpdated,
  };
};

export default connect(mapStateToProps)(App);

export { App };

根据惯例,jest 将在任何名为tests 的文件夹下找到名称以.test.js 结尾的测试文件。这意味着你需要创建容器文件夹,然后创建tests 目录,并在其下创建一个App.test.js 文件。

App 现在已经导出,所以你现在可以通过添加到你的测试文件来导入它。

import { App } from '../App'

因为你正在测试独立于reduxApp 组件,任何由redux 提供的东西(例如,该组件的props ),都需要明确提供。你可以添加一些渲染测试,看看这在实践中是如何运作的。在App.test.js ,添加你的第一个测试。

import React from "react";
import { shallow } from "enzyme";
import toJson from "enzyme-to-json";
import { App } from "../App";

describe("App", () => {
  it("renders without crashing given the required props", () => {
    const props = {
      isFetching: false,
      dispatch: jest.fn(),
      selectedSubreddit: "reactjs",
      posts: [],
    };
    const wrapper = shallow(<App {...props} />);
    expect(toJson(wrapper)).toMatchSnapshot();
  });
});

在这个测试中,你要验证App 是否在所有需要的props 。通过提供道具,你是在模拟redux 在实际应用中要做的事情。jest 提供了一个模拟函数,你可以在测试中用它来代替真实函数。在本教程中,你可以用它来模拟dispatch 函数。在你的测试中,这个函数将被调用以代替实际的dispatch 函数。

你可以用npm test 命令来运行测试。jest 测试运行器启动,运行测试,并打印出一个测试运行摘要。

Test run summary - Terminal

如果你打开src/containers/__tests__/__snapshots__/App.test.js.snap ,你会发现一个组件的快照版本,显示组件的渲染输出。

继续添加几个测试来检查App 的渲染行为。 首先,添加一个测试来确保selectedSubreddit 的道具总是被传递给Picker 组件。添加这个测试,就在你现有的测试下面。

// Add this import
import Picker from "../../components/Picker";

it("sets the selectedSubreddit prop as the `value` prop on the Picker component", () => {
  const props = {
    isFetching: false,
    dispatch: jest.fn(),
    selectedSubreddit: "reactjs",
    posts: [],
  };
  const wrapper = shallow(<App {...props} />);
  // Query for the Picker component in the rendered output
  const PickerComponent = wrapper.find(Picker);
  expect(PickerComponent.props().value).toBe(props.selectedSubreddit);
});

这个测试显示了你如何轻松地使用enzyme 来查询嵌套组件,(在这个例子中,Picker ),并断言它是用正确的props 来渲染的。我强烈建议你深入研究enzyme的文档,看看它提供的测试工具的范围。

接下来,添加另一个测试来检查基于某些条件渲染的元素。在这种情况下,你要验证当isFetching 道具是false刷新按钮就会呈现。

it("renders the Refresh button when the isFetching prop is false", () => {
  const props = {
    isFetching: false,
    dispatch: jest.fn(),
    selectedSubreddit: "reactjs",
    posts: [],
  };
  const wrapper = shallow(<App {...props} />);
  expect(wrapper.find("button").length).toBe(1);
});

最后,添加一个处理一些用户互动的测试。这个测试可以验证当刷新按钮被点击时,它能派发正确的动作。

// Add this import
import * as actions from "../../actions";

// .. other tests

it("handleRefreshClick dispatches the correct actions", () => {
  const props = {
    isFetching: false,
    dispatch: jest.fn(),
    selectedSubreddit: "reactjs",
    posts: [],
  };
  // Mock event to be passed to the handleRefreshClick function
  const mockEvent = {
    preventDefault: jest.fn(),
  };
  // Mock the actions we expect to be called
  actions.invalidateSubreddit = jest.fn();
  actions.fetchPostsIfNeeded = jest.fn();

  const wrapper = shallow(<App {...props} />);
  // Call the function on the component instance, passing the mock event
  wrapper.instance().handleRefreshClick(mockEvent);

  expect(mockEvent.preventDefault).toHaveBeenCalled();
  expect(props.dispatch.mock.calls.length).toBe(3);
  expect(actions.invalidateSubreddit.mock.calls.length).toBe(1);
  expect(actions.fetchPostsIfNeeded.mock.calls.length).toBe(2);
});

首先,导入actions ,这样你就可以模拟它提供的一些功能。在测试中,你提供通常的props ,然后提供一个mockEvent 对象。使用mockEvent 对象来模拟当按钮被点击时由浏览器发送的点击事件。模拟的事件需要包含一个preventDefault 属性。这个属性应该是一个函数,因为它将在handleRefreshClick 函数中被调用。没有它,你会得到一个关于缺少属性的错误。e.preventDefault is not a function.

一旦你使用shallow 渲染组件,手动调用handleRefreshClick ,传入模拟事件以模拟在你的应用程序中调用该函数时将会发生什么。断言应用程序的以下属性。

  • event.preventDefault 应该被调用一次
  • props.dispatch 应召3次
  • actions.invalidateSubreddit 应该被调用一次
  • actions.fetchPostsIfNeeded 本应被叫到两次
    • 第一次调用发生在componentDidMount
    • 第二次调用发生在里面handleRefreshClick

为了确保你对componentDidMount 函数调用的预期是正确的,你可以在handleRefreshClick 函数调用之前加入这些断言。

const wrapper = shallow(<App {...props} />);
// The next assertions are for functions called in componentDidMount
expect(props.dispatch.mock.calls.length).toBe(1);
expect(actions.fetchPostsIfNeeded.mock.calls.length).toBe(1);

wrapper.instance().handleRefreshClick(mockEvent);

//... rest of test omitted for brevity

在这一点上,你已经测试了代码中最具挑战性的部分,这应该给你一个很好的起点,可以舒适地添加任何其他组件功能的测试。

测试Redux功能

在本节中,你将为你的应用程序的redux 相关部分添加一些测试,特别是动作和还原器。

测试动作创建者

从动作创建者开始。这个应用程序有异步和同步的动作创建器。异步动作创建者与redux-thunk 结合使用,以实现不立即产生结果的异步操作,如获取数据。同步动作创建者返回普通对象。本教程包括如何测试两者。

作为参考,这是你的src/actions/index.js 文件应该包含的内容。

export const REQUEST_POSTS = "REQUEST_POSTS";
export const RECEIVE_POSTS = "RECEIVE_POSTS";
export const SELECT_SUBREDDIT = "SELECT_SUBREDDIT";
export const INVALIDATE_SUBREDDIT = "INVALIDATE_SUBREDDIT";

export const selectSubreddit = (subreddit) => ({
  type: SELECT_SUBREDDIT,
  subreddit,
});

export const invalidateSubreddit = (subreddit) => ({
  type: INVALIDATE_SUBREDDIT,
  subreddit,
});

export const requestPosts = (subreddit) => ({
  type: REQUEST_POSTS,
  subreddit,
});

export const transformResponseBody = (json) => {
  return json.data.children.map((child) => child.data);
};

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: transformResponseBody(json),
  receivedAt: Date.now(),
});

// const fetchPosts = (subreddit) => (dispatch) => {
//   dispatch(requestPosts(subreddit));
//   return fetch(`https://www.reddit.com/r/${subreddit}.json`)
//     .then((response) => response.json())
//     .then((json) => dispatch(receivePosts(subreddit, json)));
// };

export const fetchPosts = (subreddit) => (dispatch) => {
  dispatch(requestPosts(subreddit));
  return fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then((response) => response.json())
    .then((json) => dispatch(receivePosts(subreddit, json)));
};

const shouldFetchPosts = (state, subreddit) => {
  const posts = state.postsBySubreddit[subreddit];
  if (!posts) {
    return true;
  }
  if (posts.isFetching) {
    return false;
  }
  return posts.didInvalidate;
};

export const fetchPostsIfNeeded = (subreddit) => (dispatch, getState) => {
  if (shouldFetchPosts(getState(), subreddit)) {
    return dispatch(fetchPosts(subreddit));
  }
};

你的下一个任务是创建实现测试的文件。在src/actions/ ,创建一个名为__tests__ 的文件夹,并在其中创建一个名为actions.test.js 的文件。

同步动作创建者是简单的纯函数,接受一些数据并返回一个动作对象。你的测试应该检查,给定必要的参数,动作创建者返回正确的动作。你可以用selectSubreddit 动作创建器的测试来证明这一点,它接受一个subreddit 作为参数,然后返回一个动作。

import * as actions from "../index";

describe("actions", () => {
  const subreddit = "reactjs";

  describe("selectSubreddit", () => {
    it("should create an action with a given subreddit", () => {
      const expectedAction = {
        type: actions.SELECT_SUBREDDIT,
        subreddit,
      };
      expect(actions.selectSubreddit(subreddit)).toEqual(expectedAction);
    });
  });
});

对于大多数同步动作创建器来说,这就是你需要做的所有事情。

为了使你的工作更容易,一旦你开始测试异步动作创建器,你也可以为receivePosts 动作创建器添加一个测试。这个函数是这样工作的。

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: json.data.children.map(child => child.data),
  receivedAt: Date.now()
})

在返回的动作中,你有一个发生在posts 属性中的转换。将其提取到一个新的函数调用中,该函数接受json 参数并进行你需要的转换。注意,你必须导出新的辅助函数,这样你才能在以后的测试中访问它。receivePosts 函数的新版本显示在这里。

export const transformResponseBody = (json) => {
  return json.data.children.map(child => child.data);
}

export const receivePosts = (subreddit, json) => ({
  type: RECEIVE_POSTS,
  subreddit,
  posts: transformResponseBody(json),
  receivedAt: Date.now()
})

你可能注意到,在返回的动作中,有一个receivedAt 属性,它返回Date.now() 。在你的测试中,你将跳过对这个属性的测试,因为每次函数被调用时它都会改变。你可以自己通过模拟Date.now 函数来测试,但为了本教程的目的,你可以跳过这一步。

现在你已经选择了你需要做的事情的范围,你需要为receivePosts 动作创建者添加测试。

describe("actions", () => {
  const subreddit = "reactjs";
  // Add the mockJSON response
  const mockJSON = {
    data: {
      children: [{ data: { title: "Post 1" } }, { data: { title: "Post 2" } }],
    },
  };

  // ... other tests...

  describe("receivePosts", () => {
    it("should create the expected action", () => {
      const expectedAction = {
        type: actions.RECEIVE_POSTS,
        subreddit,
        posts: actions.transformResponseBody(mockJSON),
      };
      expect(actions.receivePosts(subreddit, mockJSON)).toMatchObject(
        expectedAction
      );
    });
  });
});

注意,你使用toMatchObject ,只是为了匹配返回的动作对象的一个子集,这就排除了匹配receivedAt 的键。

其余同步动作创建者的测试遵循同样的过程,即给定一些数据,测试返回的动作是否正确。

是时候测试非同步动作创建器了,特别是fetchPosts 动作创建器。你需要做的第一件事是导出函数,你将通过在函数中添加export ,使其成为。

export const fetchPosts = (subreddit) => (dispatch) => {
  dispatch(requestPosts(subreddit));
  return fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then((response) => response.json())
    .then((json) => dispatch(receivePosts(subreddit, json)));
};

安装一些新的软件包。

npm install --save-dev fetch-mock redux-mock-store

使用fetch-mock 来模拟使用fetchredux-mock-store 进行的 HTTP 请求。这可以帮助你创建一个模拟的商店,在测试中使用。添加测试。

// Add the new imports

import thunk from "redux-thunk";
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";

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

describe("actions", () => {
  const subreddit = "reactjs";
  // Add the mockJSON response
  const mockJSON = {
    data: {
      children: [{ data: { title: "Post 1" } }, { data: { title: "Post 2" } }],
    },
  };

  // ... other tests...

  describe("fetchPosts", () => {
    afterEach(() => {
      // restore fetch() to its native implementation
      fetchMock.restore();
    });

    it("creates REQUEST_POSTS and RECEIVE_POSTS when fetching posts", () => {
      // Mock the returned data when we call the Reddit API
      fetchMock.getOnce(`https://www.reddit.com/r/${subreddit}.json`, {
        body: mockJSON,
      });

      // The sequence of actions we expect to be dispatched
      const expectedActions = [
        { type: actions.REQUEST_POSTS },
        {
          type: actions.RECEIVE_POSTS,
          subreddit,
          posts: actions.transformResponseBody(mockJSON),
        },
      ];

      // Create a store with the provided object as the initial state
      const store = mockStore({});

      return store.dispatch(actions.fetchPostsIfNeeded(subreddit)).then(() => {
        expect(store.getActions()).toMatchObject(expectedActions);
      });
    });
  });
})

从所有必要的导入开始,包括redux-thunk 。对于这种情况,你需要配置一个实际的商店,这意味着你将把中间件也应用于模拟商店。

你有一个afterEach 函数,在每次测试后运行,并确保你恢复原始的fetch 实现。这样你的模拟实现就不会在其他测试中使用。

接下来,模拟你期望的请求,并提供一个模拟的body ,作为响应体返回。然后定义你期望在调用fetchPosts 时采取的行动序列。这个序列意味着当fetchPosts ,它应该产生一个REQUEST_POSTS ,然后是RECEIVE_POSTS ,其中包括所请求的子reddit的帖子。在RECEIVE_POSTS 动作中排除receivedAt 属性,就像之前的测试一样,并在posts 关键中添加转换后的响应体,就像你之前做的那样。

接下来,创建商店,给它一些初始状态,然后调度fetchPosts 。 最后,断言应用于商店的动作列表应该与你的expectedActions 数组中的序列相匹配。

在这一点上重新运行你的测试,应该确认一切都通过了。伟大的工作!

你的行动创造者测试到此结束。接下来是如何测试还原器。

测试减速器

减速器是Redux的核心,因为它们是你更新整个应用程序的状态的方式。减速器测试应该有助于验证你的每个派发动作是否按照预期更新状态。

下面是你要测试的reducers/index.js 文件的内容。

import { combineReducers } from "redux";
import {
  SELECT_SUBREDDIT,
  INVALIDATE_SUBREDDIT,
  REQUEST_POSTS,
  RECEIVE_POSTS,
} from "../actions";

const selectedSubreddit = (state = "reactjs", action) => {
  switch (action.type) {
    case SELECT_SUBREDDIT:
      return action.subreddit;
    default:
      return state;
  }
};
const posts = (
  state = {
    isFetching: false,
    didInvalidate: false,
    items: [],
  },
  action
) => {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
      return {
        ...state,
        didInvalidate: true,
      };
    case REQUEST_POSTS:
      return {
        ...state,
        isFetching: true,
        didInvalidate: false,
      };
    case RECEIVE_POSTS:
      return {
        ...state,
        isFetching: false,
        didInvalidate: false,
        items: action.posts,
        lastUpdated: action.receivedAt,
      };
    default:
      return state;
  }
};
const postsBySubreddit = (state = {}, action) => {
  switch (action.type) {
    case INVALIDATE_SUBREDDIT:
    case RECEIVE_POSTS:
    case REQUEST_POSTS:
      return {
        ...state,
        [action.subreddit]: posts(state[action.subreddit], action),
      };
    default:
      return state;
  }
};
const rootReducer = combineReducers({
  postsBySubreddit,
  selectedSubreddit,
});
export default rootReducer;
export { postsBySubreddit, selectedSubreddit };

你的减速器文件有两个减速器,每个减速器都管理着自己的一部分状态。最终,它们将使用combineReducers 合并成一个单一的根减速器。通过添加这个片段到reducers/index.js ,导出单个的reducer函数,使测试更加方便。

export { postsBySubreddit, selectedSubreddit }

reducers 下创建一个__tests__ 目录。然后在该目录下创建一个reducers.test.js 文件。这是你的测试要去的地方。因为它是两者中最简单的,你将首先测试selectedSubreddit reducer。

import {
  SELECT_SUBREDDIT,
  INVALIDATE_SUBREDDIT,
  REQUEST_POSTS,
  RECEIVE_POSTS,
} from "../../actions";
import { postsBySubreddit, selectedSubreddit } from "../index";

describe("app reducer", () => {
  describe("selectedSubreddit", () => {
    it("should return the default state", () => {
      expect(selectedSubreddit(undefined, {})).toBe("reactjs");
    });

    it("should update the selectedSubreddit", () => {
      const subreddit = "frontend";
      const action = {
        type: SELECT_SUBREDDIT,
        subreddit,
      };
      expect(selectedSubreddit(undefined, action)).toBe(subreddit);
    });
  });
});

第一个测试检查selectedSubreddit 减速器是否正确地初始化了状态。当给定一个undefined 状态,或者一个空的动作时,它应该返回默认值,也就是设置为reactjs 。下一个检查是验证当还原器收到一个有效的动作对象时,它是否正确地更新了状态。

现在你可以转向postsBySubreddit 减速器。

describe("postsBySubreddit", () => {
  const subreddit = "frontend";

  it("should return the default state", () => {
    expect(postsBySubreddit(undefined, {})).toEqual({});
  });

  it("should handle INVALIDATE_SUBREDDIT", () => {
    const action = {
      type: INVALIDATE_SUBREDDIT,
      subreddit,
    };
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: false,
        didInvalidate: true,
        items: [],
      },
    });
  });

  it("should handle REQUEST_POSTS", () => {
    const action = {
      type: REQUEST_POSTS,
      subreddit,
    };
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: true,
        didInvalidate: false,
        items: [],
      },
    });
  });

  it("should handle RECEIVE_POSTS", () => {
    const posts = ["post 1", "post 2"];
    const receivedAt = Date.now();
    const action = {
      type: RECEIVE_POSTS,
      subreddit,
      posts,
      receivedAt,
    };
    expect(postsBySubreddit({}, action)).toEqual({
      [subreddit]: {
        isFetching: false,
        didInvalidate: false,
        items: posts,
        lastUpdated: receivedAt,
      },
    });
  });
});

首先测试它是否正确地初始化了状态。在这种情况下,默认状态是一个空对象,如第一个测试中所示。

其余动作的测试也是类似的;你要验证给定一个动作,还原器会返回预期的状态更新。subreddit 应该被设置为返回对象的键,并且嵌套对象应该根据你在还原器中的规则被更新。

你可能会注意到,减速器的共同主题是,给定一组特定的输入(初始状态和一个动作),应该返回一个新的状态。你在返回的状态上做所有的断言,以确保它是你所期望的。

教程的这一部分完成后,你已经涵盖了React和Redux应用程序的许多相同组件,你需要在一个典型的应用程序中进行测试。

Run all test suites

使用GitHub和CircleCI的持续集成

这是本教程的一部分,你在其中加入了CircleCI的持续集成。持续集成有助于确保你对代码所做的任何修改不会破坏任何现有的功能。当你推送新的代码时,无论是通过向现有的分支添加新的提交,还是通过打开拉动请求将新的分支合并到主分支,测试都会被运行。这有助于在开发过程中及早发现错误。

CircleCI配置

你需要添加的第一件事是一个配置文件,它将告诉CircleCI如何测试你的应用程序。该配置文件需要在你的根文件夹中的一个.circleci 。将其命名为config.yml

以下是用于您的示例应用程序的配置文件。

version: 2.1
orbs:
  node: circleci/node@5.0.2
jobs:
  build-and-test:
    docker:
      - image: "cimg/base:stable"
    steps:
      - checkout
      - node/install:
          node-version: "12.22"
      - node/install-packages
      - run:
          command: npm run test
workflows:
  build-and-test:
    jobs:
      - build-and-test

集成CircleCI和GitHub

花点时间确保你已经把所有的修改推送到你之前创建的GitHub仓库。现在你可以设置CircleCI来测试你的代码,只要你做任何新的修改。

下面是如何将项目添加到CircleCI中。

在CircleCI项目视图中,点击设置项目

Setup project CircleCI

在下一个屏幕上,选择包含配置文件的分支。

Select config branch CircleCI

勾选build-and-test 工作流程中的所有步骤。

All steps view in CircleCI

点击一个步骤的名称,以获得更多关于它的细节。例如,打开npm run test 步骤,显示所有的测试。

Step details in CircleCI

恭喜你!你的CI流程已经建立起来了。你的 CI 流程已经设置好了,任何在版本库中推送的新提交都会触发测试运行,以确保你的改动不会破坏构建。如果你所做的修改导致测试失败,你会得到通知,你可以准确地追踪是哪个提交导致失败。