《深刻浅出React和Redux》(4) - 服务器通讯、单元测试

与服务器通讯

与服务器通讯的时长不可控,须要采用异步的形式,可使用js的fetch函数来调用api。html

fetch函数

fetch函数的基本使用形式为:react

fetch(apiUrl).then((response) => {
  if (response.status !== 200) {
    throw new Error('Fail to get response with status ' + response.status);
  }

  response.json().then((responseJson) => {
    this.setState(...);
  }).catch((error) => {
    this.setState(...);
  });
}).catch((error) => {
  this.setState(...);
});

以纯react(没有引入redux)的代码为例,fetch函数执行时会当即返回一个Promise类型的对象,因此要接then和catch,只要服务器返回的是合法的HTTP响应(包括500、400),都会触发then调用,因此在then回调函数中还须要判断status是否为200。
此外,即便在response.status为200时,也不能直接读取response中的内容,由于fetch在接收到HTTP响应的报头部分就会调用then,而不是等到整个HTTP响应完成。因此为了获取到body,还须要继续调用json()并针对其返回的Promise提供回调函数。
在终于成功获取到服务器返回的内容后,经过触发状态的变化引起页面的从新渲染。npm

redux-thunk中间件

redux的单向数据流是同步操做,如何实现调用服务器这样的异步操做呢?可使用redux-thunk中间件。json

npm install --save redux-thunk

在Redux架构下,一个action对象在经过store.dispatch派发,在调用reducer函数以前,会先通过一个中间件的环节,这就是产生异步操做的机会。redux

要产生异步操做要发送异步action对象,与普通的action对象不一样,它并无type字段,并且它是一个函数。而redux-thunk的工做是检查action对象是否是函数,若是不是函数就放行,完成普通action对象的生命周期,而若是发现action对象是函数,那就执行这个函数,并把Store的dispatch函数和getState函数做为参数传递到函数中去,处理过程到此为止,不会让这个异步action对象继续往前派发到reducer函数。
createStore时,将redux-thunk中间件做为storeEnhancer之一传入:api

const middlewares = [thunkMiddleware];

const storeEnhancers = compose(
  applyMiddleware(...middlewares),
  (win && win.devToolsExtension) ? win.devToolsExtension() : (f) => f,
);

export default createStore(reducer, {}, storeEnhancers);

异步操做有固定的模式,首先定义三种action类型,分别表示异步操做开始、成功、失败:浏览器

export const FETCH_STARTED = 'WEATHER/FENTCH_STARTED';
export const FETCH_SUCCESS = 'WEATHER/FENTCH_SUCCESS';
export const FETCH_FAILURE = 'WEATHER/FENTCH_FAILURE';

而后定义生成异步action的函数,这个函数会被redux-thunk截获并调用,调用时传递的参数是dispatch函数和getState函数:服务器

export const sampleAsyncAction = ()=>{
  return (dispatch, getState) => {
    
    // 在这里dispatch FETCH_STARTED action

    return fetch(apiUrl).then((response) => {
      if (response.status !== 200) {
        throw new Error('Fail to get response with status ' + response.status);
      }

      response.json().then((responseJson) => {
        // 在这里dispatch FETCH_SUCCESS action
      }).catch((error) => {
        
      });
    }).catch((error) => {
        // 在这里dispatch FETCH_FAILURE action
    });
  }
}

终止异步操做

在页面与服务端交互过程当中,每每会有一次请求还没结束就发出下一次请求的状况,好比在选择一个下拉项后调用API加载数据,若是数据还没加载完成,就切换到别的下拉项,会发生什么,这取决于两次请求的返回顺序,若是第一次请求先返回,那么页面会先显示第一次的响应结果,再刷新为第二次的,总体来讲问题不大;可是若是第二次的请求先于第一次的返回,那么页面显示的最终结果就与下拉项不匹配了。
对于这种场景,简单点能够经过加载数据时禁用下拉框来解决,但这种作法的用户体验较差,若是服务端一直没有响应,下拉框就一直处于禁用状态;还有更合理的一种作法时,在发出下一次API请求时,终止上一次的API请求。网络

惋惜ES6的标准中,Promise对象是没法中断的,为此能够经过应用层的修改来丢弃上一次的请求。以fetchWeather异步action举例:架构

export const fetchWeather = (cityCode) => {
    return (dispatch) => {
        const seqId=++nextSeqId;

        const dispatchIfValid=(action)=>{
            if(seqId===nextSeqId){
                return dispatch(action);
            }
        }

        dispatchIfValid(fetchWeatherStarted());

        const apiUrl = `/data/cityinfo/${cityCode}.html`;

        // dispatch(fetchWeatherStarted());

        return fetch(apiUrl).then((response) => {
            if (response.status !== 200) {
                throw new Error('Fail to get response with status ' + response)
            }
            response.json().then((response) => {
                dispatchIfValid(fetchWeatherSuccess(response.weatherinfo));
            }).catch((error) => {
                throw new Error('Invalid js on response: ' + error);
            });
        }).catch((error) => {
            dispatchIfValid(fetchWeatherFailure(error));
        });
    }
};

在action构造函数文件中定义一个文件模块级的nextSeqId变量,这是一个递增的整数数字,给每个访问API的请求作序列编号。在fetchWeather返回的函数中,fetch开始一个异步请求以前,先给nextSeqId自增长一,而后自增的结果赋值给一个局部变量seqId,这个seqId的值就是这一次异步请求的编号,若是随后还有fetchWeather构造器被调用,那么nextSeqId也会自增,新的异步请求会分配为新的seqId。

而后,action构造函数中全部的dispatch函数都被替换为一个新定义的函数dispatchIfValid,这个dispatchIfValid函数会检查当前环境的seqId是否等同于全局的nextSeqId。若是相同,说明fetchWeather没有被再次调用,就继续使用dispatch函数。若是不相同,说明这期间有新的fetchWeather被调用,也就是有新的访问服务器的请求被发出去了,这时候当前seqId表明的请求就已通过时了,直接丢弃掉,不须要dispatch任何action。

单元测试

关于单元测试框架的选择,因为在create-react-app建立的应用中已经自带了Jest库,因此就直接使用Jest。
Jest会自动在当前目录下寻找文件名以.test.js为后缀的文件和存放在__test__目录下的代码文件,来执行单元测试。
单元测试代码的组织方式,一般有两种模式:

  • 把所有测试代码放在与src平行的test目录,在test目录下创建和src对应子目录结构,每一个单元测试文件都加上test.js后缀,这种方法能够保持src目录的整洁,但缺点是单元测试中引用功能代码的路径会比较长;
  • 在src的子目录下建立__test__目录,用于存放对应这个目录的单元测试,这种方法的优缺点与第一种相反。

React & Redux 应用的测试对象主要有action构造函数、reducer、view,其中reducer、普通的action构造函数都是纯函数,很是便于测试。
但异步action的构造函数和view的测试相对比较复杂。

异步action的构造函数的测试

一个异步action对象就是一个函数,须要结合redux-thunk之类的中间件才能发挥做用,异步action被派发以后,会连续派发另外两个action对象表明fetch开始和fetch结束,单元测试要作的就是验证这样的行为。
中间件的应用和action的dispatch都涉及到Redux Store,但单元测试中并不须要建立一个完整功能的Store,也不该该进行真实的网络访问。因此须要一些测试辅助工具。
其中可使用redux-mock-store来建立一个mock store:

npm install -save-dev redux-mock-store

使用sinon来“篡改”fetch函数的行为,使其不会发出真实的网络请求:

npm install -save-dev sinon

而后就能够开始测试了,首先须要作一些准备工做:

Create Mock Store

import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
const middlewares = [thunk];
const createMockStore = configureStore(middlewares);
...
const store = createMockStore();

Create Mock Store后,就能够像真实store同样在其上dispatch action了。

“篡改”fetch函数的行为

import { stub } from 'sinon';
...
let stubbedFetch;

beforeEach(() => {
  stubbedFetch = stub(global, 'fetch');
});

afterEach(() => {
  stubbedFetch.restore();
});

const mockResponse = Promise.resolve({
  status: 200,
  json: () => Promise.resolve({
    weatherinfo: {}
  })
});
stubbedFetch.returns(mockResponse);

使用sinon的stub函数来覆盖fetch的返回结果,单元测试用例之间应该互不影响,因此stubbedFetch应该在beforeEach中执行,并在测试用例跑完时执行afterEach时恢复stub行为。

React组件的测试

测试React组件,测试的是渲染结果、事件处理。
但并非全部的测试过程都须要把React组件的DOM树都渲染出来,尤为对于包含复杂子组件的React组件,若是深刻渲染整个DOM树,那就要渲染全部子组件,但是子组件可能会有其余依赖关系,好比依赖于某个React Context值,为了渲染这样的子组件须要耗费不少精力准备测试环境,这种状况下针对目标组件的测试只要让它渲染顶层组件就行了,不须要测试子组件。
测试React组件能够借助Enzyme,它由AirBnb开源,enzyme依赖react-addons-test-utils,要一块儿安装:

npm install -save-dev enzyme react-addons-test-utils

Enzyme支持三种渲染方法:

  • shallow,只渲染顶层React组件,不渲染子组件,适合只测试React组件的渲染行为;
  • mount,渲染完整的React组件包括子组件,借助模拟的浏览器环境完成事件处理功能;
  • render,渲染完整的React组件,可是只产生HTML,并不进行事件处理。

无状态React组件的测试,可使用shallow方法只渲染一层,忽略子组件是为了简化测试过程。举例:

const wrapper = shallow(<Filter />);
expect(wrapper.contains(<Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>)).toBe(true);

被链接的React组件的测试,被链接的React组件是指状态保存在Redux的Store上,并经过connect函数产生的组件,这种组件使用时须要包裹在Provider中,测试的时候也同样,并且还会测试事件处理、action dispatch后引起视图的变化,因此这里须要使用真实的store。

import { Provider } from 'react-redux';
...

const subject = (
  <Provider store={store}>
    <待测组件 />
  </Provider>);

参考书籍

《深刻浅出React和Redux》 程墨

相关文章
相关标签/搜索