今天补上上次新闻客户端欠下的单元测试。新闻客户端github地址:点我,接上篇博客

本次单元测试用到了单元测试比较流行的测试框架mocha,用到的是expect断言库,和react官方的测试插件:react-addons-test-utils。

那本次单元测试的地址在github上另起一个分支,来区别一下两次提交。本次单元测试地址:点我

npm install && npm test 即可测试该项目

通过本次单元测试,不仅添加了测试,还发现了原先作品的一些问题,这也是函数式编程所注意的地方。

这是test文件夹的目录:

和redux官方例子的目录是一样的,我仅仅把内容改了一下。

react-redux的作品相对来说还是很好写测试的,由于redux是函数式编程的思想,关于redux的单元测试就像测试js函数一样方便。麻烦的就是react的测试,它需要模拟用户的操作,而且还需要区分虚拟dom和真实dom,在测试的时候我们会把它渲染在真实dom当中。

那么问题来了,测试环境并没有浏览器的dom环境,没有window,document这些东西咋办呢,node有个包叫jsdom,我们在测试之前,先用jsdom模拟一下浏览器的环境:

  1. import { jsdom } from 'jsdom'
  2.  
  3. global.document = jsdom('<!doctype html><html><body></body></html>')
  4. global.window = document.defaultView
  5. global.navigator = global.window.navigator

一些jsdom的api的应用,其中模拟了document,window和navigator,怎么把它添加到测试中呢?

我们再看一下测试mocha的测试命令,摘抄自package.json

  1. "scripts": {
  2. "start": "node server.js",
  3. "test": "NODE_ENV=test mocha --recursive --compilers js:babel-core/register --require ./test/setup.js",
  4. "test:watch": "npm test -- --watch"
  5. },

首先设置NODE_ENV,关于webpack热启动,如果是winodws用户遇到NODE_ENV不是命令请看关于windows下NODE_ENV=test无效的情况解决办法

启动mocha -h可以看到--require来加载setup.js也就是jsdom模拟的环境。这句命令就是先把这些文件通过babel编译一下,再引入jsdom模拟环境。

那我们开始正式来说测试吧。

看目录可知,我们测试分4个部分,测试actionscomponentscontainersreducers

与redux有关的就是actions和reducers,components是测试组件是否正确调用了该方法,containers是测试组件是否正常工作。后面2个都是react-redux的东西啦,跟我们函数式的redux可没关系。

我们先把函数式的redux的相关测试写上--

测试actions

我们需要知道每个actionCreator是否正确了返回action,我觉得这东西用眼就能看出来。。真是没有必要测试,不过人家官网写了,我也写上吧。

顺便说一句,expect断言库真的是蛮好用的,和chai的expect类似。推荐一下expect和chai。

  1. import expect from 'expect'
  2. import * as actions from '../../actions/counter'
  3.  
  4. describe('actions', () => {
  5. it('increment1 should create increment1 action', () => {
  6. expect(actions.increment1()).toEqual({ type: actions.LOVE_COUNTER_ONE })
  7. })
  8. it('increment2 should create increment1 action', () => {
  9. expect(actions.increment2()).toEqual({ type: actions.LOVE_COUNTER_TWO })
  10. })
  11. it('increment3 should create increment1 action', () => {
  12. expect(actions.increment3()).toEqual({ type: actions.LOVE_COUNTER_THREE })
  13. })
  14.  
  15. it('text1 should create text1 action', () => {
  16. expect(actions.text1("any1")).toEqual({ type: actions.TXST_COUNTER_ONE,text:"any1" })
  17. })
  18. it('text2 should create text1 action', () => {
  19. expect(actions.text2("any2")).toEqual({ type: actions.TXST_COUNTER_TWO,text:"any2" })
  20. })
  21. it('text3 should create text1 action', () => {
  22. expect(actions.text3("any3")).toEqual({ type: actions.TXST_COUNTER_THREE,text:"any3" })
  23. })
  24.  
  25. it('hf1 should create hf1 action', () => {
  26. expect(actions.hf1(1,"any1")).toEqual({ type: actions.HF_COUNTER_ONE,id:1,hf:"any1" })
  27. })
  28. it('hf2 should create hf1 action', () => {
  29. expect(actions.hf2(2,"any2")).toEqual({ type: actions.HF_COUNTER_TWO,id:2,hf:"any2" })
  30. })
  31. it('hf3 should create hf1 action', () => {
  32. expect(actions.hf3(3,"any3")).toEqual({ type: actions.HF_COUNTER_THREE,id:3,hf:"any3"})
  33. })
  34. })

二,测试reducers

  reducers也比较简单,首先引入reducers文件和相关action

  1. import expect from 'expect'
  2. import {counter} from '../../reducers/counter'
  3. import {content} from '../../reducers/counter'
  4. import { LOVE_COUNTER_ONE,LOVE_COUNTER_TWO,LOVE_COUNTER_THREE } from '../../actions/counter'
  5. import { TXST_COUNTER_ONE,TXST_COUNTER_TWO,TXST_COUNTER_THREE } from '../../actions/counter'
  6. import { HF_COUNTER_ONE,HF_COUNTER_TWO,HF_COUNTER_THREE } from '../../actions/counter'

首先是counter的测试,它功能是啥来?就是点击心♥,♥后面的数字会加,然后根据心来排序。

  1. describe('reducers', () => {
  2. describe('counter', () => {
  3. const initailState={
  4. one:{id:1,counter:0,title:"好险,库里将到手的锅一脚踢飞!",time:1 },
  5. two:{id:2,counter:0,title:"中国男足赔率1:501!",time:42},
  6. three:{id:3,counter:0,title:"为什么要善待高洪波和宫鲁鸣",time:1}
  7. };
  8. it('should handle initial state', () => {
  9. expect(counter(undefined, {})).toEqual(initailState);
  10. })
  11.  
  12. it('should handle LOVE_COUNTER_ONE', () => {
  13. const state={
  14. one:{id:1,counter:1,title:"好险,库里将到手的锅一脚踢飞!",time:1 },
  15. two:{id:2,counter:0,title:"中国男足赔率1:501!",time:42},
  16. three:{id:3,counter:0,title:"为什么要善待高洪波和宫鲁鸣",time:1}
  17. };
  18. expect(counter(initailState, { type: LOVE_COUNTER_ONE })).toEqual(state);
  19. })
  20.  
  21. it('should handle LOVE_COUNTER_TWO', () => {
  22. const state={
  23. one:{id:1,counter:0,title:"好险,库里将到手的锅一脚踢飞!",time:1 },
  24. two:{id:2,counter:1,title:"中国男足赔率1:501!",time:42},
  25. three:{id:3,counter:0,title:"为什么要善待高洪波和宫鲁鸣",time:1}
  26. };
  27. expect(counter(initailState, { type: LOVE_COUNTER_TWO })).toEqual(state);
  28. })
  29.  
  30. it('should handle LOVE_COUNTER_THREE', () => {
  31. const state={
  32. one:{id:1,counter:0,title:"好险,库里将到手的锅一脚踢飞!",time:1 },
  33. two:{id:2,counter:0,title:"中国男足赔率1:501!",time:42},
  34. three:{id:3,counter:1,title:"为什么要善待高洪波和宫鲁鸣",time:1}
  35. };
  36. expect(counter(initailState, { type: LOVE_COUNTER_THREE })).toEqual(state);
  37. })
  38. it('should handle unknown action type', () => {
  39. expect(counter(initailState, { type: 'unknown' })).toEqual(initailState);
  40. })
  41. })

看代码这么多,其实主要的代码就几句,首先给reducer一个初始state。

然后期望传入初始state,传入每个action.type得到不同的state。就像普通的js函数那么好测试。

expect(counter(initailState, { type: LOVE_COUNTER_THREE })).toEqual(state);

type是每个action的type,看是不是toEqual改变后的state。


当时我在这里出错了,写单元测试,怎么改都不对,mocha提示我说那里出错了,真是一找就找到错误了(再次推荐抹茶~)

原来是我的reducers不”纯“。(错误的代码不影响效果,但是是错误的,错误代码可以看本github的master分支)

函数式编程的要求就是函数要纯。restful的API大火,它强调状态要幂等。类似函数的“纯”。


我看了一下第一个确实有问题,这里上一下代码片段:

  1. return Object.assign({},state,{one:{id:1,counter:++state.one.counter,title:state.one.title,time:state.one.time }})

虽然我用了assign函数保证了state是不变的,但是还是顺手写了个++,然后state就变了。晕。。然后就改为了+1,测试果然过了。

然后第二个就郁闷了,第二个仔细看确实没啥问题,大家看出哪不纯了么?

  1. const newState=state.concat();
  2. newState[0].push({text:action.text,huifu:[]})
  3. return newState;
  4. // var data=[
  5. // [
  6. // {
  7. // text:"这里是评论1",
  8. // huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
  9. // },
  10. // {
  11. // text:"这里是评论1.2",
  12. // huifu:["huifuxxxxxxxxxxxxx"]
  13. // }
  14. // ],[
  15. // {
  16. // text:"这里是评论2",
  17. // huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
  18. // }
  19. // ],[
  20. // {
  21. // text:"这里是评论3",
  22. // huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
  23. // }
  24. // ]
  25. // ]
  26. /*

其实你只看reducer代码是看不出啥的,state是个数组,我concat()复制一个数组,再操作复制后的newState,有啥问题??

然而固执的单元测试就说我这不纯,。后来仔细看才发现,确实不纯。。

newState.push(xxxx),ok没问题,纯的,newState[0].push(xxx),不行,不纯了,state已经改变了。好吧,确实改变了。因为数组里面的数组没复制,newState还是引用原来的地址。。

于是牵扯到对象的深克隆。。于是手写了一个深克隆,果然测试通过了。上一次我的deepClone:

  1. function deepClone(obj){
  2. var res=Array.isArray(obj)?[]:{};
  3. for(var key in obj){
  4. if (typeof obj[key]=="object") {
  5. res[key]=deepClone(obj[key]);
  6. }else{
  7. res[key]=obj[key];
  8. }
  9. }
  10. return res;
  11. }

这里巧妙地用了typeof的坑,typeof obj和array都会返回“object”。

然后reducer的state.concat()就变成了deepClone(state);

三,测试components

这个是测试compoents的,就是说测试react组件的运行情况,原理就是看它是不是dispatch了相应事件。

首先引入react-addons-test-utils,和Counter组件,还findDOMNode,这是react提供的获得真实组件的方法,现在被转移到react-dom里面,后来又推荐用refs获取真实dom了,包括在

react-addons-test-utils API上面都是用的refs。

  1. import expect from 'expect'
  2. import React from 'react'
  3. import TestUtils from 'react-addons-test-utils'
  4. import Counter from '../../components/Counter'
  5. import {findDOMNode} from 'react-dom'

react-addons-test-utils有啥用呢?该api地址:点我

列出我们用到的方法:

renderIntoDocument() //渲染组件到真实dom环境

scryRenderedDOMComponentsWithClass() //从渲染的dom环境中根据class选取真实dom,它的结果是个结果集

                      //相对的还有findRenderedDOMComponentsWithClass,不同的是它结果只有一个而已

Simulate.click()  //模拟用户点击

Simulate.change() //用于改变对应dom

准备活动~

  1. function setup() {
  2. const actions = {
  3. increment1: expect.createSpy(),
  4. increment2: expect.createSpy(),
  5. increment3: expect.createSpy(),
  6.  
  7. text1: expect.createSpy(),
  8. text2: expect.createSpy(),
  9. text3: expect.createSpy(),
  10.  
  11. hf1: expect.createSpy(),
  12. hf2: expect.createSpy(),
  13. hf3: expect.createSpy()
  14. }
  15. const initailCounter={
  16. one:{id:1,counter:0,title:"xxxx" ,time:1},
  17. two:{id:2,counter:0,title:"xxxx", time:1},
  18. three:{id:3,counter:0,title:"xxxx",time:1}
  19. }
  20. const initailContent=[
  21. [{text:"这里是评论1",huifu: ["huifuxxxxxxxxxxxxx"] },{text:"这里是评论1.2",huifu:[]}],
  22. [{text:"这里是评论2",huifu:["huifuxxxxxxxxxxxxx"]}],
  23. [{text:"这里是评论3",huifu:["huifuxxxxxxxxxxxxx"]} ]
  24. ];
  25. const component = TestUtils.renderIntoDocument(<Counter content={initailContent} counter={initailCounter} {...actions} />)
  26. return {
  27. component: component,
  28. actions: actions,
  29. heart:TestUtils.scryRenderedDOMComponentsWithClass(component,"heart"),
  30. heartNum: TestUtils.scryRenderedDOMComponentsWithClass(component, 'heart')
  31. }
  32. }

expect.createSpy()创建一个可以追踪的函数,用这个可以看到它是不是被调用了。

然后是TestUtils.renderIntoDocument(<Counter content={initailContent} counter={initailCounter} {...actions} />);

渲染完组件,导出一些用到的东西,heart是渲染组件里的class为heart的dom,点击它心会+1;heartNum就是存放心数量的div啦。

  1. describe('Counter component', () => {
  2. it('should display heart number', () => {
  3. const { heartNum } = setup()
  4. expect(heartNum[0].textContent).toMatch(/^0/g)
  5. })
  6.  
  7. it('click first heart should call increment1', () => {
  8. const { heart, actions } = setup()
  9. TestUtils.Simulate.click(heart[0])
  10. expect(actions.increment1).toHaveBeenCalled()
  11. })
  12.  
  13. it('pinglun2 buttons should call text2', () => {
  14. const {actions,component } = setup()
  15. const realDom=findDOMNode(component);
  16. const plbtn=realDom.querySelectorAll('.plbtn');
  17. TestUtils.Simulate.click(plbtn[1])
  18. const pingl=TestUtils.scryRenderedDOMComponentsWithClass(component, 'pingl');
  19. TestUtils.Simulate.click(pingl[0]);
  20. expect(actions.text2).toHaveBeenCalled()
  21. })
  22.  
  23. it('huifu3 button should call hf3', () => {
  24. const { actions,component } = setup()
  25. const realDom=findDOMNode(component);
  26. const plbtn=realDom.querySelectorAll('.plbtn');
  27. TestUtils.Simulate.click(plbtn[2]);//点击评论
  28. const hf=TestUtils.scryRenderedDOMComponentsWithClass(component, 'hf');
  29. TestUtils.Simulate.click(hf[0]);//点击回复
  30. const hfBtn=TestUtils.scryRenderedDOMComponentsWithClass(component, 'hf-btn');
  31. TestUtils.Simulate.click(hfBtn[0]);//点击回复
  32. expect(actions.hf3).toHaveBeenCalled()
  33. })
  34. })

第一个希望心的数量match 0,初始化的时候。然后是模拟点击,点击心会触发increment1,点击评论2号的评论的提交按钮会调用text2方法。点击回复3号的按钮会触发hf3方法。

就是自己点击写期望的结果,就像真正在点击浏览器一样,不多说了。

注意一点,scryRenderedDOMComponentsWithClass支持的css选择器很少,一般可以用findDOMNode这个东西,找到该渲染后的dom,用querySelectorAll就方便多了。

四,测试containers

这个测试就像是测试了,,它是关注你组件的结果,不管程序咋样,我满足你的条件,你得给我我想要的结果。

原理就是把组件渲染到dom里,dispatch一下,然后查看结果。结果咋查看?就看该出现评论的地方有没有输入的字样。match匹配一下。

准备工作~

  1. import expect from 'expect'
  2. import React from 'react'
  3. import TestUtils from 'react-addons-test-utils'
  4. import { Provider } from 'react-redux'
  5. import App from '../../containers/App'
  6. import configureStore from '../../store/configureStore'
  7. import { findDOMNode } from "react-dom"

看到了,我们在这个测试里面直接把react-redux那一套创建store的方法拿出来了。

  1. function setup(initialState) {
  2. const store = configureStore(initialState)
  3. const app = TestUtils.renderIntoDocument(
  4. <Provider store={store}>
  5. <App />
  6. </Provider>
  7. )
  8. return {
  9. app: app,
  10. heart: TestUtils.scryRenderedDOMComponentsWithClass(app, 'heart'),
  11. heartNum: TestUtils.scryRenderedDOMComponentsWithClass(app, 'heart')
  12. }
  13. }

把组件渲染进去,开始测试。

也是蛮简单的。我这里就只测试评论和回复的功能了。

  1. const { buttons, p,app } = setup()
  2. const realDom=findDOMNode(app);
  3. const plbtn=realDom.querySelectorAll('.plbtn');
  4. TestUtils.Simulate.click(plbtn[0]);//点击评论
  5. const plInput=realDom.querySelectorAll(".pl-input")[0];
  6. plInput.value="any111";
  7. TestUtils.Simulate.change(plInput);//input输入any111
  8. const pingl=TestUtils.scryRenderedDOMComponentsWithClass(app, 'pingl');
  9. TestUtils.Simulate.click(pingl[0]);//点击提交
  10. const text=realDom.querySelectorAll('.body-text p');
  11. expect(text[text.length-1].textContent).toMatch(/^any111/)

这里只列出测试评论的代码吧。

和上个一样,乱七八糟的获取dom,然后模拟点击,这里用到了模拟输入,plInput.value="any111";TestUtils.Simulate.change(plInput);

固定api,没啥好说的。其实还有好几个测试,我只是写了代表性的一部分。剩下的都是雷同的,就不写了~

完毕~谢谢~

react-redux单元测试(基于react-addons-test-utils,mocha)的更多相关文章

  1. React-Native(三):React Native是基于React设计的

    React Native是基于React js设计的. 参考:<React 入门实例教程> React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript ...

  2. 一个 React & Redux的目录树

    |-----------------------------------------| | | | React & Redux | | | |------------------------- ...

  3. React Native 系列(二) -- React入门知识

    前言 本系列是基于React Native版本号0.44.3写的,最初学习React Native的时候,完全没有接触过React和JS,本文的目的是为了给那些JS和React小白提供一个快速入门,让 ...

  4. 基于 React.js + Redux + Bootstrap 的 Ruby China 示例 (转)

    一直学 REACT + METEOR 但路由部分有点问题,参考一下:基于 React.js + Redux + Bootstrap 的 Ruby China 示例 http://react-china ...

  5. 实例讲解基于 React+Redux 的前端开发流程

    原文地址:https://segmentfault.com/a/1190000005356568 前言:在当下的前端界,react 和 redux 发展得如火如荼,react 在 github 的 s ...

  6. 写了一个基于React+Redux的仿Github进度条

    曾经实现过Angular版,这次感觉用了高大上的React却写了更多的代码,需要的配置也更多了,有利有弊吧. 但这个“导航条问题”很有意思,涉及到在Redux中写timer,其实我很困惑,到底如何完美 ...

  7. 基于React实现的【绿色版电子书阅读器】,支持离线下载

    代码地址如下:http://www.demodashi.com/demo/12052.html MyReader 绿色版电子书阅读器 在线地址:http://myreader.linxins.com ...

  8. 基于React和Node.JS的表单录入系统的设计与实现

    一.写在前面 这是一个真实的项目,项目已经过去好久了,虽然很简单,但还是有很多思考点,跟随着笔者的脚步,一起来看看吧.本文纯属虚构,涉及到的相关信息均已做虚构处理, 二.背景 人活着一定要有信仰,没有 ...

  9. webpack+react+redux+es6开发模式

    一.预备知识 node, npm, react, redux, es6, webpack 二.学习资源 ECMAScript 6入门 React和Redux的连接react-redux Redux 入 ...

  10. webpack+react+redux+es6

    一.预备知识 node, npm, react, redux, es6, webpack 二.学习资源 ECMAScript 6入门 React和Redux的连接react-redux Redux 入 ...

随机推荐

  1. VS2015 + OPENCV + CUDA 安装流程

    VS2015  https://blog.csdn.net/guxiaonuan/article/details/73775519?locationNum=2&fps=1 OPENCV htt ...

  2. PhpStorm本地断点调试

    一.断点调试php环境搭建 1.检测本地php环境是否安装了Xdebug 在本地输出phpinfo():搜索Xdebug;如下图  如果没有安装,安装操作Xdebug如下: 将phpinfo();的信 ...

  3. app自动化测试Appium+python

    一.node.js安装 https://nodejs.org/en/download/ ##一直下一步 ###cmd查看 二.  .NET Framework安装 https://www.micros ...

  4. shit iview docs & i-radio bug

    shit iview docs & i-radio bug https://github.com/iview/iview/issues/5627 <i-row> <i-col ...

  5. list类型功能剖析

    append  向后追加 name_list=["eirc","alex","tony"] name_list.append('seven' ...

  6. 相识mongodb

    1.下载完安装包,并解压下载地址:https://www.mongodb.org/dl/linux/x86_64或者可以直接wget http://fastdl.mongodb.org/linux/m ...

  7. vim的几个常用操作

    现在很少会有人用vim来写代码,所以vim更常用在server上面编辑配置文件或者少量代码编辑: vim操作命令非常之多,如果仅用作一个配置文件的编辑器,掌握几个常用的操作就够了: 常用的操作其实就是 ...

  8. CodeForces - 1051D Bicolorings(DP)

    题目链接:http://codeforces.com/problemset/problem/1051/D 看了大佬的题解后觉着是简单的dp,咋自己做就做不来呢. 大佬的题解:https://www.c ...

  9. 通过流量清理防御DDoS

    导读 在2018年2月,世界上最大的分布式拒绝服务(DDoS)攻击在发起20分钟内得到控制,这主要得益于事先部署的DDoS防护服务. 这次攻击是针对GitHub–数百万开发人员使用的主流在线代码管理服 ...

  10. [代码]--给任意网站添加聊天功能,随时聊(fa)天(che)

    感谢“topurl.cn”制作此功能并分享. 这是一段代码,在打开的网页中使用,可以加载一个外挂形式的聊天室功能, 就可以和同样访问此网站进行相同操作的网友进行聊(fa)天(che)了. 使用方法: ...