十二、测试和调试

本章将介绍以下配方:

  • 用 Jest 和酶测试我们的第一个成分
  • 测试 Redux 容器、操作和还原程序
  • 使用 React 和 Redux 开发工具调试 React 应用
  • 模拟事件

介绍

对于任何想要获得高质量的项目来说,测试和调试都是非常重要的。不幸的是,许多开发人员并不关心测试(单元测试),因为他们认为这会降低开发速度,有些开发人员会将其留到项目结束。以我个人的经验,我可以说,从项目一开始就进行测试将节省您的时间,因为最终,您将有更少的 bug 需要修复。React 使用 Jest 测试其组件、容器、操作和还原程序。

在下面的方法中,我们还将学习如何调试 React/Redux 应用。

用 Jest 和酶测试我们的第一个成分

在本教程中,我们将学习如何在项目中安装和配置 Jest。

准备

在此配方中,我们需要安装几个软件包来测试 React 应用:

npm install --save-dev jest jsdom enzyme enzyme-adapter-react-16 identity-obj-proxy

怎么做。。。

安装 Jest 后,我们需要对其进行配置:

  1. tests脚本和 Jest 配置添加到我们的package.json中:
  {
    "name": "react-pro",
    "version": "1.0.0",
    "scripts": {
      "clean": "rm -rf dist/ && rm -rf public/app",
      "start": "npm run clean & NODE_ENV=development 
      BABEL_ENV=development nodemon src/server --watch src/server --
      watch src/shared --exec babel-node --presets es2015",
      "start-analyzer": "npm run clean && NODE_ENV=development 
      BABEL_ENV=development ANALYZER=true babel-node src/server",
      "test": "node scripts/test.js src --env=jsdom",
      "coverage": "node scripts/test.js src --coverage --env=jsdom"
    },
    "jest": {
      "setupTestFrameworkScriptFile": "
 <rootDir>/config/jest/setupTestFramework.js",
      "collectCoverageFrom": [
        "src/**/*.{js,jsx}"
      ],
      "setupFiles": [
        "<rootDir>/config/jest/browserMocks.js"
      ],
      "moduleNameMapper": {
        "^.+\\.(scss)$": "identity-obj-proxy"
      }
    },
    "author": "Carlos Santana",
    "license": "MIT",
    "dependencies": {
      "axios": "^0.18.0",
      "babel-preset-stage-0": "^6.24.1",
      "express": "^4.15.4",
      "react": "^16.3.2",
      "react-dom": "^16.3.2",
      "react-redux": "^5.0.6",
      "react-router-dom": "^4.2.2",
      "redux": "^4.0.0",
      "redux-devtools-extension": "^2.13.2",
      "redux-thunk": "^2.2.0"
    },
    "devDependencies": {
      "babel-cli": "^6.26.0",
      "babel-core": "^6.26.0",
      "babel-eslint": "^8.2.3",
      "babel-loader": "^7.1.2",
      "babel-plugin-module-resolver": "^3.1.1",
      "babel-preset-env": "^1.6.0",
      "babel-preset-es2015": "^6.24.1",
      "babel-preset-react": "^6.24.1",
      "compression-webpack-plugin": "^1.0.0",
      "css-loader": "^0.28.5",
      "enzyme": "^3.3.0",
      "enzyme-adapter-react-16": "^1.1.1",
      "eslint": "^4.5.0",
      "eslint-plugin-babel": "^5.1.0",
      "eslint-plugin-import": "^2.7.0",
      "eslint-plugin-jsx-a11y": "^6.0.2",
      "eslint-plugin-react": "^7.8.2",
      "eslint-plugin-standard": "^3.0.1",
      "extract-text-webpack-plugin": "4.0.0-beta.0",
      "husky": "^0.14.3",
      "identity-obj-proxy": "^3.0.0",
      "jest": "^23.1.0",
      "jsdom": "^11.11.0",
      "node-sass": "^4.5.3",
      "nodemon": "^1.17.4",
      "react-hot-loader": "^4.2.0",
      "redux-mock-store": "^1.5.1",
      "sass-loader": "^7.0.1",
      "style-loader": "^0.21.0",
      "webpack": "^4.8.3",
      "webpack-bundle-analyzer": "^2.9.0",
      "webpack-dev-middleware": "^3.1.3",
      "webpack-hot-middleware": "^2.18.2",
      "webpack-hot-server-middleware": "^0.5.0",
      "webpack-merge": "^4.1.0",
      "webpack-node-externals": "^1.6.0",
      "webpack-notifier": "^1.6.0"
    }
  }

File: package.json

  1. 正如您在 Jest 配置中所看到的,我们需要添加setupTestFramework.js文件,在该文件中,我们将配置酶以将其与 Jest 一起使用:
  import { configure } from 'enzyme';
  import Adapter from 'enzyme-adapter-react-16';

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

File: config/jest/setupTestFramework.js

  1. setupFiles节点中,我们可以指定我们的browserMocks.js文件,在这里我们可以模拟我们在应用中使用的任何浏览器方法。例如,如果你想在你的应用中测试localStorage,这个文件是模拟它的合适位置:
  // Browser Mocks
  const requestAnimationFrameMock = callback => {
    setTimeout(callback, 0);
  };

  Object.defineProperty(window, 'requestAnimationFrame', {
    value: requestAnimationFrameMock
  });

  const localStorageMock = (() => {
    let store = {}

    return {
      getItem: key => store[key] || null,
      setItem: (key, value) => store[key] = value.toString(),
      removeItem: key => delete store[key],
      clear: () => store = {}
    };
  })();

  Object.defineProperty(window, 'localStorage', {
    value: localStorageMock
  });

File: config/jest/browserMocks.js

  1. 如果您在组件中使用 Sass、Stylus 或更少,则需要使用正则表达式指定moduleNameMapper模式,以匹配项目中的所有.scss文件(或.styl/.less),并使用identity-obj-proxy处理这些文件,该软件包模拟 Webpack 导入,如 CSS 模块。
  2. 您可能已经注意到,我们添加了两个新的 NPM 脚本:一个用于测试我们的应用,另一个用于获得覆盖率(覆盖单元测试的百分比)。对于这些,我们正在使用一个特定的脚本,它位于scripts/test.js,让我们创建该文件:
  // Set the NODE_ENV to test
  process.env.NODE_ENV = 'test';

  // Requiring jest
  const jest = require('jest');

  // Getting the arguments from the terminal
  const argv = process.argv.slice(2);

  // Runing Jest passing the arguments
  jest.run(argv);

File: scripts/test.js

  1. 让我们假设我们有这个Home组件:
  import React from 'react';
  import styles from './Home.scss';

  const Home = props => (
    <h1 className={styles.Home}>Hello {props.name || 'World'}</h1>
  );

 export default Home;

File: src/client/home/index.jsx

  1. 如果您想测试这个组件,您需要创建一个同名的文件,但在文件中添加.test后缀。在这种情况下,我们的测试文件将命名为index.test.jsx
  // Dependencies
  import React from 'react';
  import { shallow } from 'enzyme';

  // Component to test...
  import Home from './index';

  describe('Home', () => {
    const subject = shallow(<Home />);
    const subjectWithProps = shallow(<Home name="Carlos" />);

    it('should render Home component', () => {
      expect(subject.length).toBe(1);
    });

    it('should render by default Hello World', () => {
      expect(subject.text()).toBe('Hello World');
    });

    it('should render the name prop', () => {
      expect(subjectWithProps.text()).toBe('Hello Carlos');
    });

    it('should has .Home class', () => {
      expect(subject.find('h1').hasClass('Home')).toBe(true);
    });
  });

File: src/client/home/index.test.jsx

它是如何工作的。。。

如果要测试应用,需要运行以下命令:

    npm test

如果您的测试是正确的,您应该看到以下结果:

PASS标签表示您在该文件中的所有测试都成功通过;如果您至少一次测试失败,您将看到FAIL标签。让我们修改一下我们的"should has .Home class测试。我要将值更改为"Home2"以强制失败:

如您所见,现在我们得到了FAIL标签,并用 X 指定了失败的测试。此外,ExpectedReceived值提供了有用的信息,通过这些信息,我们可以看到预期值和接收值。

还有更多。。。

现在,如果要查看所有单元测试的覆盖率,可以使用以下命令:

 npm run coverage

目前,我们的Home组件只有一个单元测试,正如您所看到的,它是绿色的,在 100%时,所有其他文件都是红色的,带有 0%,因为这些文件尚未测试:

另外,coverage 命令生成结果的 HTML 版本。有一个目录名为"coverage",另一个目录名为"Icov-report"。如果在浏览器中打开index.html,您将看到如下内容:

测试 Redux 容器、操作和还原程序

在这个配方中,我们将测试 Redux 容器、操作和还原程序。对于本例,我们将测试在第 11 章中创建的 Todo 列表,实现服务器端渲染。

Remember always that we use an existing recipe you must run npm install command first to restore all the project dependencies otherwise, you will get dependency errors.

准备

我们需要安装redux-mock-storemoxiosredux-thunk包来测试我们的 Redux 容器。您需要先运行npm install来安装所有依赖项:

    npm install // This is to install the previous packages
    npm install redux-mock-store moxios redux-thunk

怎么做。。。

让我们测试我们的 Redux 容器:

  1. Redux 容器不应该有任何 JSX 代码;最佳实践是在我们的connect方法中让mapStateToPropsmapDispatchToProps在导出中传递另一个组件(例如Layout组件),例如,让我们看看我们的 Todo 列表容器:
  // Dependencies
  import { connect } from 'react-redux';
  import { bindActionCreators } from 'redux';

 // Components
  import Layout from '../components/Layout';

  // Actions
  import { fetchTodo } from '../actions';

  export default connect(({ todo }) => ({
    todo: todo.list
  }), dispatch => bindActionCreators(
    {
      fetchTodo
    },
    dispatch
  ))(Layout);

File: src/client/todo/container/index.js

  1. 你可能想知道我们到底需要在这里测试什么。嗯,我们需要在容器中测试的最重要的事情是动作分派(即fetchTodo动作),并使用数据从 Redux 获取我们的todo状态。也就是说,这是我们的容器单元测试文件:
  // Dependencies
  import React from 'react';
  import { shallow } from 'enzyme';
  import configureStore from 'redux-mock-store';

  // Actions
  import { fetchTodo } from '../actions';

  // Testable Container
  import Container from './index';

  // Mocking Initial State
  const mockInitialState = {
    todo: {
      list: [
        {
          id: 1,
          title: 'Go to the Gym'
        },
        {
          id: 2,
          title: 'Dentist Appointment'
        },
        {
          id: 3,
          title: 'Finish homework'
        }
      ]
    }
  };

  // Configuring Mock Store
  const mockStore = configureStore()(mockInitialState);

  // Mocking the Actions
  jest.mock('../actions', () => ({
    fetchTodo: jest.fn().mockReturnValue({ type: 'mock-FETCH_TODO_SUCCESS' })
  }));

  describe('Todo Container', () => {
    let mockParams;
    let container;

    beforeEach(() => {
      fetchTodo.mockClear();
      mockParams = {};
      mockStore.clearActions();
      container = shallow(<Container {...mockParams} store={mockStore} />);
    });

    it('should dispatch fetchTodo', () => {
      const { fetchTodo } = container.props();

      fetchTodo();

      const actions = mockStore.getActions();

      expect(actions).toEqual([{ type: 'mock-FETCH_TODO_SUCCESS' }]);
    });

    it('should map todo and get the todo list from Initial State', () => {
      const { todo } = container.props();
      const { todo: { list }} = mockInitialState;

      expect(todo).toEqual(list);
    });
  });

File: src/client/todo/container/index.test.js

  1. 测试fetchTodo动作。这是我们的操作文件的代码:
  // Base Actions
  import { request, received } from '@baseActions';

 // Api
  import api from '../api';

  // Action Types
  import { FETCH_TODO } from './actionTypes';

  export const fetchTodo = () => dispatch => {
    const action = FETCH_TODO;
    const { fetchTodo } = api;

    dispatch(request(action));

    return fetchTodo()
      .then(response => dispatch(received(action, response.data)));
  };

File: src/client/todo/actions/index.js

  1. 这是我们的actionTypes.js文件:
  // Actions
 export const FETCH_TODO = {
    request: () => 'FETCH_TODO_REQUEST',
    success: () => 'FETCH_TODO_SUCCESS'
  };

File: src/client/todo/actions/actionTypes.js

  1. 要测试异步 Redux 操作,我们需要使用redux-thunkmoxios来测试使用axios从服务器检索数据的操作。我们的测试文件应该如下所示:
  // Dependencies
  import configureMockStore from 'redux-mock-store';
  import thunk from 'redux-thunk';
  import moxios from 'moxios';

  // Action
  import { fetchTodo } from './index';

  // Action Types
  import { FETCH_TODO } from './actionTypes';

  // Configuring Store with Thunk middleware
  const mockStore = configureMockStore([thunk]);

  // Response Mock
  const todoResponseMock = [
    {
      id: 1,
      title: 'Go to the Gym'
    },
    {
      id: 2,
      title: 'Dentist Appointment'
    },
    {
      id: 3,
      title: 'Finish homework'
    }
  ];

  describe('fetchTodo action', () => {
    beforeEach(() => {
      moxios.install();
    });

    afterEach(() => {
      moxios.uninstall();
    });

    it('should fetch the Todo List', () => {
      moxios.wait(() => {
        const req = moxios.requests.mostRecent();

        req.respondWith({
          status: 200,
          response: todoResponseMock
        });
      });

      const expectedActions = [
        {
          type: FETCH_TODO.request()
        },
        {
          type: FETCH_TODO.success(),
          payload: todoResponseMock
        }
      ];

      const store = mockStore({ todo: [] })

      return store.dispatch(fetchTodo()).then(() => {
        expect(store.getActions()).toEqual(expectedActions);
      });
    });
  });

File: src/client/todo/actions/index.test.js

  1. 让我们测试一下减速器。这是 Todo reducer 文件:
  // Utils
  import { getNewState } from '@utils/frontend';

 // Action Types
  import { FETCH_TODO } from '../actions/actionTypes';

  // Initial State
  const initialState = {
    list: []
  };

  export default function todoReducer(state = initialState, action) {
    switch (action.type) {
      case FETCH_TODO.success(): {
        const { payload: { response = [] } } = action;

        return getNewState(state, {
          list: response
        });
      }

      default:
        return state;
    }
  }

File: src/client/todo/reducer/index.js

  1. 我们需要在减速器中测试两件事:初始状态和FETCH_TODO操作成功时的状态:
  // Reducer
  import todo from './index';

  // Action Types
  import { FETCH_TODO } from '../actions/actionTypes';

 // Initial State
  const initialState = {
    list: []
  };

  describe('Todo List Reducer', () => {
    it('should return the initial state', () => {
      const expectedInitialState = todo(undefined, {});

      expect(expectedInitialState).toEqual(initialState);
    });

    it('should handle FETCH_TODO when is success', () => {
      const action = {
        type: FETCH_TODO.success(),
        payload: {
          response: [
            {
              id: 1,
              title: 'Go to the Gym'
            },
            {
              id: 2,
              title: 'Dentist Appointment'
            },
            {
              id: 3,
              title: 'Finish homework'
            }
          ]
        }
      };

      const expectedState = {
        list: action.payload.response
      };

      const state = todo(initialState, action);

      expect(state).toEqual(expectedState);
    });
  });

File: src/client/todo/reducer/index.test.js

使用 React 和 Redux 开发工具调试 React 应用

调试对于任何应用都是必不可少的,它帮助我们识别和修复 bug。Chrome 有两个强大的工具来调试 React/Redux 应用,并将其集成到开发人员工具中。React 开发工具和 Redux 开发工具。

准备

使用 Google Chrome,您必须安装两个扩展:

此外,您还需要安装redux-devtools-extension软件包:

npm install --save-dev redux-devtools-extension 

一旦安装了 React 开发者工具和 Redux 开发者工具,就需要对它们进行配置。

如果您试图直接使用 Redux DevTools,它将不起作用;这是因为我们需要将composeWithDevTools方法传递到我们的 Redux 存储中,这应该是我们的configureStore.js文件:

  // Dependencies
  import { createStore, applyMiddleware } from 'redux';
  import thunk from 'redux-thunk';
  import { composeWithDevTools } from 'redux-devtools-extension';

  // Root Reducer
  import rootReducer from '@reducers';

 export default function configureStore({ initialState, appName, 
  reducer }) {
    const middleware = [
      thunk
    ];

    return createStore(
      rootReducer,
      initialState,
      composeWithDevTools(applyMiddleware(...middleware))
    );
  }

File: src/shared/redux/configureStore.js

怎么做。。。

让我们调试应用:

  1. 如果要调试 React 应用,请使用 Google Chrome(http://localhost:3000/todo)打开应用,打开 Google 开发工具(右键单击>检查),选择 React 选项卡,您将看到 React 组件:

  1. 您可以选择要调试的组件,最酷的事情之一是您可以在右侧看到组件的道具:

  1. 如果要在应用中调试 Redux 并查看正在调度哪些操作,则需要在 Chrome 开发工具中选择 Redux 选项卡:

  1. 我们在 Todo 应用中调度两个操作:FETCH_TODO_REQUESTFETCH_TODO_SUCCESS。在 Redux 中,@@INIT操作在默认情况下被调度,这在任何应用中都会发生。
  2. 如果您选择FETCH_TODO_REQUEST操作,您将看到在 Diff 选项卡上显示,“(状态相等)”。这意味着该操作中没有任何更改,但您有四个选项卡:操作、状态、差异和测试。

  3. 如果选择“操作”选项卡,则可以看到该特定操作:

  1. 如果选择FETCH_TODO_SUCCESS,您将看到 todo 减速器的数据:

模拟事件

在这个配方中,我们将学习如何在一个简单的计算器组件上模拟onClickonChange事件。

怎么做。。。

我们将重复使用上一个配方的代码(Repository: Chapter12/Recipe3/debugging

  1. 我们将创建一个简单的Calculator组件,将两个值相加(输入),然后当用户点击等于(=按钮时,我们将得到结果:
  import React, { Component } from 'react';
  import styles from './Calculator.scss';

  class Calculator extends Component {
    state = {
      number1: 0,
      number2: 0,
      result: 0
    };

    handleOnChange = e => {
      const { target: { value, name } } = e;

      this.setState({
        [name]: value
      });
    }

    handleResult = () => {
      this.setState({
        result: Number(this.state.number1) + Number(this.state.number2)
      });
    }

    render() {
      return (
        <div className={styles.Calculator}>
          <h1>Calculator</h1>

          <input
            name="number1"
            value={this.state.number1}
            onChange={this.handleOnChange}
          />

          {' + '}

          <input
            name="number2"
            value={this.state.number2}
            onChange={this.handleOnChange}
          />

          <button onClick={this.handleResult}>
            =
          </button>

          <input
            name="result"
            value={this.state.result}
          />
        </div>
      );
    }
  }

  export default Calculator;

File: src/client/calculator/index.jsx

  1. 如果要在浏览器中查看此组件(它是为测试目的创建的),则需要将其包含在 routes 文件中:
  import React from 'react';
  import { Switch, Route } from 'react-router-dom';

 // Components
  import Calculator from '../../client/calculator';

  const paths = [
    {
      component: Calculator,
      exact: true,
      path: '/'
    }
  ];

  const all = (
    <Switch>
      <Route exact path={paths[0].path} component={paths[0].component} />
    </Switch>
  );

 export default {
    paths,
    all
  };

File: src/shared/routes/index.jsx

  1. 如果您想看到一些基本样式,我们可以使用这些:
  .Calculator {
    padding: 100px;

    input {
        width: 50px;
        height: 50px;
        padding: 40px;
        font-size: 24px;
    }

    button {
        padding: 10px;
        margin: 10px;
    }
  }

File: src/client/calculator/Calculator.scss

  1. 在我们的测试文件中,我们需要模拟onChange事件来更改输入的值,然后模拟点击等于(=按钮:
  // Dependencies
  import React from 'react';
  import { shallow } from 'enzyme';

  // Component to test...
  import Calculator from './index';

  describe('Calculator', () => {
    const subject = shallow(<Calculator />);

    it('should render Calculator component', () => {
      expect(subject.length).toBe(1);
    });

    it('should modify the state onChange', () => {
      subject.find('input[name="number1"]').simulate('change', {
        target: {
          name: 'number1',
          value: 5
        }
      });

      subject.find('input[name="number2"]').simulate('change', {
        target: {
          name: 'number2',
          value: 15
        }
      });

      // Getting the values of the number1 and number2 states
      expect(subject.state('number1')).toBe(5);
      expect(subject.state('number2')).toBe(15);
    });

    it('should perform the sum when the user clicks the = button', 
    () => {
      // Simulating the click event
      subject.find('button').simulate('click');

      // Getting the result value
      expect(subject.state('result')).toBe(20);
    });
  });

它是如何工作的。。。

如果您想在浏览器中查看组件,请使用npm start运行应用,您将看到如下内容:

现在,让我们使用npm test命令测试计算器: