react-redux單元測試(基於react-addons-test-utils,mocha)

今天補上上次新聞客戶端欠下的單元測試。新聞客戶端github地址:點我,接上篇博客css

本次單元測試用到了單元測試比較流行的測試框架mocha,用到的是expect斷言庫,和react官方的測試插件:react-addons-test-utils。html

那本次單元測試的地址在github上另起一個分支,來區別一下兩次提交。本次單元測試地址:點我node

npm install && npm test 便可測試該項目react

經過本次單元測試,不只添加了測試,還發現了原先做品的一些問題,這也是函數式編程所注意的地方。webpack

這是test文件夾的目錄:git

和redux官方例子的目錄是同樣的,我僅僅把內容改了一下。github

react-redux的做品相對來講仍是很好寫測試的,因爲redux是函數式編程的思想,關於redux的單元測試就像測試js函數同樣方便。麻煩的就是react的測試,它須要模擬用戶的操做,並且還須要區分虛擬dom和真實dom,在測試的時候咱們會把它渲染在真實dom當中。web

那麼問題來了,測試環境並無瀏覽器的dom環境,沒有window,document這些東西咋辦呢,node有個包叫jsdom,咱們在測試以前,先用jsdom模擬一下瀏覽器的環境:npm

import { jsdom } from 'jsdom'

global.document = jsdom('<!doctype html><html><body></body></html>')
global.window = document.defaultView
global.navigator = global.window.navigator

一些jsdom的api的應用,其中模擬了document,window和navigator,怎麼把它添加到測試中呢?編程

咱們再看一下測試mocha的測試命令,摘抄自package.json

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

首先設置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。

import expect from 'expect'
import * as actions from '../../actions/counter'

describe('actions', () => {
  it('increment1 should create increment1 action', () => {
    expect(actions.increment1()).toEqual({ type: actions.LOVE_COUNTER_ONE })
  })
  it('increment2 should create increment1 action', () => {
    expect(actions.increment2()).toEqual({ type: actions.LOVE_COUNTER_TWO })
  })
  it('increment3 should create increment1 action', () => {
    expect(actions.increment3()).toEqual({ type: actions.LOVE_COUNTER_THREE })
  })

  it('text1 should create text1 action', () => {
    expect(actions.text1("any1")).toEqual({ type: actions.TXST_COUNTER_ONE,text:"any1" })
  })
  it('text2 should create text1 action', () => {
    expect(actions.text2("any2")).toEqual({ type: actions.TXST_COUNTER_TWO,text:"any2" })
  })
  it('text3 should create text1 action', () => {
    expect(actions.text3("any3")).toEqual({ type: actions.TXST_COUNTER_THREE,text:"any3" })
  })

  it('hf1 should create hf1 action', () => {
    expect(actions.hf1(1,"any1")).toEqual({ type: actions.HF_COUNTER_ONE,id:1,hf:"any1" })
  })
  it('hf2 should create hf1 action', () => {
    expect(actions.hf2(2,"any2")).toEqual({ type: actions.HF_COUNTER_TWO,id:2,hf:"any2" })
  })
  it('hf3 should create hf1 action', () => {
    expect(actions.hf3(3,"any3")).toEqual({ type: actions.HF_COUNTER_THREE,id:3,hf:"any3"})
  })
})

二,測試reducers

  reducers也比較簡單,首先引入reducers文件和相關action

import expect from 'expect'
import {counter} from '../../reducers/counter'
import {content} from '../../reducers/counter'
import { LOVE_COUNTER_ONE,LOVE_COUNTER_TWO,LOVE_COUNTER_THREE } from '../../actions/counter'
import { TXST_COUNTER_ONE,TXST_COUNTER_TWO,TXST_COUNTER_THREE } from '../../actions/counter'
import { HF_COUNTER_ONE,HF_COUNTER_TWO,HF_COUNTER_THREE } from '../../actions/counter'

首先是counter的測試,它功能是啥來?就是點擊心♥,♥後面的數字會加,而後根據心來排序。

describe('reducers', () => {
  describe('counter', () => {
    const initailState={
      one:{id:1,counter:0,title:"好險,庫裏將到手的鍋一腳踢飛!",time:1 },
      two:{id:2,counter:0,title:"中國男足賠率1:501!",time:42},
      three:{id:3,counter:0,title:"爲何要善待高洪波和宮魯鳴",time:1}
    };
    it('should handle initial state', () => {
      expect(counter(undefined, {})).toEqual(initailState);
    })

    it('should handle LOVE_COUNTER_ONE', () => {
      const state={
        one:{id:1,counter:1,title:"好險,庫裏將到手的鍋一腳踢飛!",time:1 },
        two:{id:2,counter:0,title:"中國男足賠率1:501!",time:42},
        three:{id:3,counter:0,title:"爲何要善待高洪波和宮魯鳴",time:1}
      };
      expect(counter(initailState, { type: LOVE_COUNTER_ONE })).toEqual(state);
    })

    it('should handle LOVE_COUNTER_TWO', () => {
      const state={
        one:{id:1,counter:0,title:"好險,庫裏將到手的鍋一腳踢飛!",time:1 },
        two:{id:2,counter:1,title:"中國男足賠率1:501!",time:42},
        three:{id:3,counter:0,title:"爲何要善待高洪波和宮魯鳴",time:1}
      };
      expect(counter(initailState, { type: LOVE_COUNTER_TWO })).toEqual(state);
    })

    it('should handle LOVE_COUNTER_THREE', () => {
      const state={
        one:{id:1,counter:0,title:"好險,庫裏將到手的鍋一腳踢飛!",time:1 },
        two:{id:2,counter:0,title:"中國男足賠率1:501!",time:42},
        three:{id:3,counter:1,title:"爲何要善待高洪波和宮魯鳴",time:1}
      };
      expect(counter(initailState, { type: LOVE_COUNTER_THREE })).toEqual(state);
    })
    it('should handle unknown action type', () => {
      expect(counter(initailState, { type: 'unknown' })).toEqual(initailState);
    })
  })

看代碼這麼多,其實主要的代碼就幾句,首先給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大火,它強調狀態要冪等。相似函數的「純」。


我看了一下第一個確實有問題,這裏上一下代碼片斷:

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

雖然我用了assign函數保證了state是不變的,可是仍是順手寫了個++,而後state就變了。暈。。而後就改成了+1,測試果真過了。

而後第二個就鬱悶了,第二個仔細看確實沒啥問題,你們看出哪不純了麼?

 const newState=state.concat();
 newState[0].push({text:action.text,huifu:[]})
 return newState;
 // var data=[
 //      [
 //         {
 //            text:"這裏是評論1",
 //            huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
 //          },
 //         {
 //            text:"這裏是評論1.2",
 //            huifu:["huifuxxxxxxxxxxxxx"]
 //          }
 //      ],[
 //          {
 //            text:"這裏是評論2",
 //            huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
 //          }
 //      ],[
 //          {
 //            text:"這裏是評論3",
 //            huifu:["huifuxxxxxxxxxxxxx",'2xxxxxxxxxxxxxxxxxxxx','3xxxxxxxxxxxxxxxxx']
 //          }
 //        ]  
 //    ]
/*

其實你只看reducer代碼是看不出啥的,state是個數組,我concat()複製一個數組,再操做複製後的newState,有啥問題??

然而執拗的單元測試就說我這不純,。後來仔細看才發現,確實不純。。

newState.push(xxxx),ok沒問題,純的,newState[0].push(xxx),不行,不純了,state已經改變了。好吧,確實改變了。由於數組裏面的數組沒複製,newState仍是引用原來的地址。。

因而牽扯到對象的深克隆。。因而手寫了一個深克隆,果真測試經過了。上一次個人deepClone:

function deepClone(obj){
  var res=Array.isArray(obj)?[]:{};
  for(var key in obj){
    if (typeof obj[key]=="object") {
      res[key]=deepClone(obj[key]);
    }else{
      res[key]=obj[key];
    }
  }
  return res;
}

這裏巧妙地用了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。

import expect from 'expect'
import React from 'react'
import TestUtils from 'react-addons-test-utils'
import Counter from '../../components/Counter'
import {findDOMNode} from 'react-dom'

react-addons-test-utils有啥用呢?該api地址:點我

列出咱們用到的方法: 

renderIntoDocument() //渲染組件到真實dom環境

scryRenderedDOMComponentsWithClass() //從渲染的dom環境中根據class選取真實dom,它的結果是個結果集

                      //相對的還有findRenderedDOMComponentsWithClass,不一樣的是它結果只有一個而已

Simulate.click()  //模擬用戶點擊

Simulate.change() //用於改變對應dom

準備活動~

function setup() {
  const actions = {
    increment1: expect.createSpy(),
    increment2: expect.createSpy(),
    increment3: expect.createSpy(),

    text1: expect.createSpy(),
    text2: expect.createSpy(),
    text3: expect.createSpy(),

    hf1: expect.createSpy(),
    hf2: expect.createSpy(),
    hf3: expect.createSpy()
  }
  const initailCounter={
    one:{id:1,counter:0,title:"xxxx" ,time:1},
    two:{id:2,counter:0,title:"xxxx", time:1},
    three:{id:3,counter:0,title:"xxxx",time:1}
  }
  const initailContent=[ 
       [{text:"這裏是評論1",huifu: ["huifuxxxxxxxxxxxxx"] },{text:"這裏是評論1.2",huifu:[]}],
       [{text:"這裏是評論2",huifu:["huifuxxxxxxxxxxxxx"]}],
       [{text:"這裏是評論3",huifu:["huifuxxxxxxxxxxxxx"]} ]
   ];
  const component = TestUtils.renderIntoDocument(<Counter content={initailContent} counter={initailCounter} {...actions} />)
  return {
    component: component,
    actions: actions,
    heart:TestUtils.scryRenderedDOMComponentsWithClass(component,"heart"),
    heartNum: TestUtils.scryRenderedDOMComponentsWithClass(component, 'heart')
  }
}

expect.createSpy()建立一個能夠追蹤的函數,用這個能夠看到它是否是被調用了。

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

渲染完組件,導出一些用到的東西,heart是渲染組件裏的class爲heart的dom,點擊它心會+1;heartNum就是存放心數量的div啦。

describe('Counter component', () => {
  it('should display heart number', () => {
    const { heartNum } = setup()
    expect(heartNum[0].textContent).toMatch(/^0/g)
  })

  it('click first heart should call increment1', () => {
    const { heart, actions } = setup()
    TestUtils.Simulate.click(heart[0])
    expect(actions.increment1).toHaveBeenCalled()
  })

  it('pinglun2 buttons should call text2', () => {
    const {actions,component } = setup()
     const realDom=findDOMNode(component);
     const plbtn=realDom.querySelectorAll('.plbtn');
    TestUtils.Simulate.click(plbtn[1])
    const pingl=TestUtils.scryRenderedDOMComponentsWithClass(component, 'pingl');
    TestUtils.Simulate.click(pingl[0]);
    expect(actions.text2).toHaveBeenCalled()
  })

  it('huifu3 button should call hf3', () => {
    const { actions,component } = setup()
    const realDom=findDOMNode(component);
     const plbtn=realDom.querySelectorAll('.plbtn');
    TestUtils.Simulate.click(plbtn[2]);//點擊評論
      const hf=TestUtils.scryRenderedDOMComponentsWithClass(component, 'hf');
    TestUtils.Simulate.click(hf[0]);//點擊回覆
    const hfBtn=TestUtils.scryRenderedDOMComponentsWithClass(component, 'hf-btn');
    TestUtils.Simulate.click(hfBtn[0]);//點擊回覆
    expect(actions.hf3).toHaveBeenCalled()
  })
})

第一個但願心的數量match 0,初始化的時候。而後是模擬點擊,點擊心會觸發increment1,點擊評論2號的評論的提交按鈕會調用text2方法。點擊回覆3號的按鈕會觸發hf3方法。

就是本身點擊寫指望的結果,就像真正在點擊瀏覽器同樣,很少說了。

注意一點,scryRenderedDOMComponentsWithClass支持的css選擇器不多,通常能夠用findDOMNode這個東西,找到該渲染後的dom,用querySelectorAll就方便多了。

四,測試containers

這個測試就像是測試了,,它是關注你組件的結果,無論程序咋樣,我知足你的條件,你得給我我想要的結果。

原理就是把組件渲染到dom裏,dispatch一下,而後查看結果。結果咋查看?就看該出現評論的地方有沒有輸入的字樣。match匹配一下。

準備工做~

import expect from 'expect'
import React from 'react'
import TestUtils from 'react-addons-test-utils'
import { Provider } from 'react-redux'
import App from '../../containers/App'
import configureStore from '../../store/configureStore'
import { findDOMNode } from "react-dom"

看到了,咱們在這個測試裏面直接把react-redux那一套建立store的方法拿出來了。

function setup(initialState) {
  const store = configureStore(initialState)
  const app = TestUtils.renderIntoDocument(
    <Provider store={store}>
      <App />
    </Provider>
  )
  return {
    app: app,
    heart: TestUtils.scryRenderedDOMComponentsWithClass(app, 'heart'),
    heartNum: TestUtils.scryRenderedDOMComponentsWithClass(app, 'heart')
  }
}

把組件渲染進去,開始測試。

也是蠻簡單的。我這裏就只測試評論和回覆的功能了。

  const { buttons, p,app } = setup()
  const realDom=findDOMNode(app);
  const plbtn=realDom.querySelectorAll('.plbtn');
  TestUtils.Simulate.click(plbtn[0]);//點擊評論
  const plInput=realDom.querySelectorAll(".pl-input")[0];
  plInput.value="any111";
  TestUtils.Simulate.change(plInput);//input輸入any111
  const pingl=TestUtils.scryRenderedDOMComponentsWithClass(app, 'pingl');
  TestUtils.Simulate.click(pingl[0]);//點擊提交
  const text=realDom.querySelectorAll('.body-text p');
  expect(text[text.length-1].textContent).toMatch(/^any111/)

這裏只列出測試評論的代碼吧。

和上個同樣,亂七八糟的獲取dom,而後模擬點擊,這裏用到了模擬輸入,plInput.value="any111";TestUtils.Simulate.change(plInput);

固定api,沒啥好說的。其實還有好幾個測試,我只是寫了表明性的一部分。剩下的都是雷同的,就不寫了~

 

完畢~謝謝~

相關文章
相關標籤/搜索