[译] 测试 React & Redux 应用良心指南

测试 React & Redux 应用良心指南

前端只是一层薄薄的静态页面的时代已经一去不复返了。现代 web 应用程序变得愈来愈复杂,逻辑也持续从后端向前端转移。然而,当涉及到测试时,许多人都保持着过期的心态。若是你使用的是 React 和 Redux,可是因为某些缘由对测试你的代码不感兴趣,我将在这里向你展现如何以及为何咱们天天都这样作。前端

注意:我将使用 JestEnzyme。它们是测试 React & Redux 应用最流行的工具。我猜你已经用过或者能熟练使用它们了。react

单元测试和集成测试简单对比

React & Redux 应用构建在三个基本的构建块上:actions、reducers 和 components。是独立测试它们(单元测试),仍是一块儿测试(集成测试)取决于你。集成测试会覆盖到整个功能,能够把它想成一个黑盒子,而单元测试专一于特定的构建块。从个人经验来看,集成测试很是适用于容易增加但相对简单的应用。另外一方面,单元测试更适用于逻辑复杂的应用。尽管大多数应用都适合第一种状况,但我将从单元测试开始更好地解释应用层。android

咱们将构建(并测试)什么

这里有一个可用的 应用。当你第一次进入页面的时候,不会显示图片。你能够经过点击按钮来获取一张图片。我使用了免费的 Dog API。如今让咱们写一些测试。能够查看个人 源码ios

单元测试:Action 建立函数

为了展现一只狗的图片,咱们首先要获取它,若是你不熟悉 thunk,别担忧。Thunk 是一个中间件,它能够给咱们返回一个函数,而不是 action 对象。咱们能够用它根据 HTTP 请求结果来 dispatch 对应的成功的 action 或者失败的 action。git

咱们要测试从 API 成功取回的数据是否 dispatch 了成功的 action,而且将数据一块儿传递。为了作到这一点,咱们将使用 redux-mock-storegithub

注意:我使用 axios 来做为客户端请求工具,用 axios-mock-adapter 来 mock 实际 API 的请求。你能够自由选择适合你的工具。web

import configureMockStore from 'redux-mock-store';
import { FETCH_DOG_REQUEST, FETCH_DOG_SUCCESS } from '../../constants/actionTypes';
import fetchDog from './fetchDog';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

describe('fetchDog action', () => {

  let store;
  let httpMock;

  const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));

  beforeEach(() => {
    httpMock = new MockAdapter(axios);
    const mockStore = configureMockStore();
    store = mockStore({});
  });

  it('fetches a dog', async () => {
    // given
    httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {
      status: 'success',
      message: 'https://dog.ceo/api/img/someDog.jpg',
    });
    // when
    fetchDog()(store.dispatch);
    await flushAllPromises();
    // then
    expect(store.getActions()).toEqual(
      [
        { type: FETCH_DOG_REQUEST },
        { payload: { url: 'https://dog.ceo/api/img/someDog.jpg' }, type: FETCH_DOG_SUCCESS }
      ]);
  })
});
复制代码

一开始,让咱们在 beforeEach() 中进行 mock store 和模拟的 http 客户端的初始化。在测试中,咱们为请求指定结果。以后,执行咱们的 action 建立函数。由于咱们使用了 thunk,所以它会返回一个函数,咱们把 store 的 dispatch 方法传给这个函数。在进行任何断言以前,请求须要变为 resolved,所以咱们要确保没有 pending 的 Promise。redux

const flushAllPromises = () => new Promise(resolve => setImmediate(resolve));
复制代码

这行代码会把全部的 promise 放到一个单独的事件循环中。window.setImmediate 是用来在浏览器已经完成了好比事件和显示更新等其余操做后,结束这些长时间运行的操做,并当即执行它的回调函数。 在这个例子中,挂起的 HTTP 请求就是咱们要完成的操做。此外,因为这不是一个标准的浏览器特性,因此你不该该在正式代码中使用它。axios

单元测试:Reducers

我认为 reducers 是应用程序的核心。若是你开发功能丰富、复杂的系统,这部分就会变得很复杂。若是你引入了一个 bug,之后可能很难查找。这就是为何测试 reducers 很是重要。咱们正在构建的应用很是简单,但我但愿你能获取到图片。后端

每一个 reducer 都会在应用启动时被调用,所以须要一个初始状态。听任你的初始状态为 undefined 会让你在组件中写好多校验代码。

it('returns initial state', () => {
    expect(dogReducer(undefined, {})).toEqual({url: ''});
  });
复制代码

这段代码很直接,咱们使用 undefined 的状态运行 reducer,并检查它是否会返回带有初始值的状态。

咱们还必须保证那个 reducer 能正确的响应成功的请求,并获取到图片的 URL。

it('sets up fetched dog url', () => {
    // given
    const beforeState = {url: ''};
    const action = {type: FETCH_DOG_SUCCESS, payload: {url: 'https://dog.ceo/api/img/someDog.jpg'}};
    // when
    const afterState = dogReducer(beforeState, action);
    // then
    expect(afterState).toEqual({url: 'https://dog.ceo/api/img/someDog.jpg'});
  });
复制代码

Reducers 应该是纯函数,没有反作用。这会让测试它们变得很是简单。提供一个以前的状态,触发一个 action,而后验证输出状态是否正确。

单元测试:Components

在咱们开始以前,让咱们先谈谈组件有哪些方面值得测试。咱们显然没法测试组件是否好看。可是,咱们绝对应该测试某些条件性的元素是否能成功显示;或者对组件执行某些操做(不是 redux 中的 action),经过组件 props 传递的方法是否会被调用。

在咱们的系统中,咱们彻底依赖 redux 管理应用的状态,所以咱们全部的组件都是无状态的。

注意:若是你在寻找优雅的 Enzyme 断言库,能够查看 enzyme-matchers

组件的结构很简单。咱们有 DogApp 根组件和用来获取并显示狗的图片的 RandomDog 组件。 RandomDog 组件的 props 以下:

static propTypes = {
    dogUrl: PropTypes.string,
    fetchDog: PropTypes.func,
  };
复制代码

Enzymes 可让咱们用两种方式来渲染一个组件。Shallow Rendering 意味着只有根组件会被渲染。若是你把 shallow rendered 组件的文本打印出来,你会发现全部子组件都没有被渲染。Shallow rendering 很是适合单独测试组件,而且从 Enzyme 3 开始(Enzyme 2 中也是可选的),它会调用生命周期的方法,好比 componentDidMount()。咱们稍后再介绍第二种方法。

如今咱们来写 RandomDog 组件的测试用例。

首先,咱们要确保没有图片 URL 时,要显示占位符,并且不该该显示图片。

it('should render a placeholder', () => {
    const wrapper = shallow(<RandomDog />);
    expect(wrapper.find('.dog-placeholder').exists()).toBe(true);
    expect(wrapper.find('.dog-image').exists()).toBe(false);
  });
复制代码

其次,在提供图片 URL 时,图片应该替换占位符显示出来。

it('should render actual dog image', () => {
    const wrapper = shallow(<RandomDog dogUrl="http://somedogurl.dog" />);
    expect(wrapper.find('.dog-placeholder').exists()).toBe(false);
    expect(wrapper.find('img[src="http://somedogurl.dog"]').exists()).toBe(true);
  });
复制代码

最后,点击获取狗的图片按钮,应该会执行 fetchDog() 方法。

it('should execute fetchDog', () => {
    const fetchDog = jest.fn();
    const wrapper = shallow(<RandomDog fetchDog={fetchDog}/>);
    wrapper.find('.dog-button').simulate('click');
    expect(fetchDog).toHaveBeenCalledTimes(1);
  });
复制代码

注意:在这个例子中,我使用了元素和类选择器。若是你发现它很脆弱并重构了代码,能够考虑切换到 custom attributes

只有单元测试,没有集成测试

我用一些陈词滥调来讲明单元测试的问题。

虽然单元测试是个很好的工具,但它并不能保证咱们正确链接了全部的组件,或者 reducer 订阅了正确的 action。这是 bug 容易发生的位置,这就是为何咱们须要集成测试。

是的,有些人认为因为上述缘由,单元测试是没用的,但我认为他们没有面对过一个足够复杂的系统来发现单元测试的价值。

集成测试

咱们如今将它们捆绑在一块儿并放在一个黑盒子中,而不是单独和详细地测试构建块。咱们再也不关心内部是如何工做的,或是组件内部究竟发生了什么。 这就是为何集成测试很是有弹性和方便重构的缘由。你能够切换整个底层机制而无需更新测试。

在集成测试中,咱们再也不须要 mock store。让咱们使用真实的吧。

import { applyMiddleware, createStore } from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducers/index';

export default function setupStore(initialState) {
  return createStore(reducers, {...initialState}, applyMiddleware(thunk));
}
复制代码

就是这样。如今,咱们有一个功能齐全的 store,是时候开始第一个测试了。咱们使用 Enzyme 的 mount 来(实现挂载类型的渲染)。Mount 很是适合集成测试,由于它会渲染整个底层组件树。

正如咱们在单元测试中所作的那样,咱们要检查应用启动时是否没有显示图像。可是如今我没有将空的图像 URL 做为组件的 prop 传递,而是将其包装在 Provider 中,传递了咱们建立的 store。

it('should render a placeholder when no dog image is fetched', () => {
    let wrapper = mount(<Provider store={store}><App /></Provider>);
    expect(wrapper.find('div.dog-placeholder').text()).toEqual('No dog loaded yet. Get some!');
    expect(wrapper.find('img.dog-image').exists()).toBe(false);
  });
复制代码

没有什么特别的是吧?咱们来看第二个测试用例。

it('should fetch and render a dog', async () => {
    httpMock.onGet('https://dog.ceo/api/breeds/image/random').reply(200, {
      status: 'success',
      message: 'https://dog.ceo/api/img/someDog.jpg'
    });

    const wrapper = mount(<Provider store={store}><App /></Provider>);
    wrapper.find('.dog-button').simulate('click');

    await flushAllPromises();
    wrapper.update();

    expect(wrapper.find('img[src="https://dog.ceo/api/img/someDog.jpg"]').exists()).toBe(true);
  });
复制代码

很容易对吧?这个测试描述了咱们和组件之间的真实交互。它涵盖了单元测试所作的每一个方面,甚至更多。如今咱们能够说构建块不只可以单独运行,并且可以以正确的方式结合起来。

哦,若是你对 Enzyme 很熟悉,还想知道我为何调用 wrapper.update(),这就是缘由。简而言之:这是 Enzyme 3 的一个 bug。也许在你阅读这篇文章时,它会被修复。

快照测试简介

Jest 提供了一种确保代码更改不会改变组件的 render()方法输出的方法。虽然编写快照测试很是简单快捷,但它们并不具备描述性,也没法经过测试驱动开发过程。我看到的惟一使用案例是,当你对其余人的未经测试的遗留代码进行一些更改时,你并不想整理这些代码,更不但愿由于修改它而受到指责。

那么咱们应该使用什么类型的测试?

只须要从集成测试开始。你极可能以为不会在你的项目中实施一个单元测试。这意味着你的复杂性不会在构建块之间划分,这样很是好。你会节省不少时间。另外一方面,有些系统会利用单元测试的能力。二者都有用武之地。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索