React UT散记

1,844 阅读5分钟

jestjs.io

一 简单配置

npx jest --init 一一配置环境 覆盖率 清除mock调用

npx jest --coverage 生成caverage目录

"scripts": {
    "coverage": "node scripts/test.js --coverage --watchAll=false"
}

package.json “scripts”: { "test": "jest --watchAll",
// a模式:监听所有测试文件变化,一个文件改了运行全部用例 "test:o": "jest --watch", // 进入o模式

}

Watch Usage

  • Press f to run only failed tests. (只运行失败的用例)
  • Press o to only run tests related to changed files. (只运行修改文件相关的用例,需配合git)
  • Press t to filter by a test name regex pattern. (根据用例的名字运行用例)
  • Press p to filterr by a filename regex pattern. (根据文件名运行用例)
  • Press q to quit watch mode. 退出监听

二 常用点

describe('类型', ()=>{
    test('test', ()=>{
        // 类型相关
        expect(2+2).toBe(4)
        expect(2+2).not.toBe(5)
        expect(Null).toBeNull()
        expect(1).toBeTruthy()
        expect(0).toBeFalsy()
        expect(undefined).toBeUndefined()
        expect(1).toBeDefined()
        // 数字相关
        expect(12).toBeGreaterThan(11)
        expect(10).toBeLessThan(11)
        expect(10).toBeGreaterThanOrEqual(10)  // >=
        expect(10).toBeLessThanOrEqual(10)  // >=
        expect(0.1 + 0.2).toBeCloseTo(0.3)
        // 字符串相关
        expect('http://www.abc.com').toMatch(/abc/)
        // Array, Set相关
        expect(['a','b','c']).toContain('b')
        // 异常
        const throwNewError = () =>{
            throw new Error('this si a new error')
        }
        expect(throwNewError).toThrow('this is a new error')
        // 匹配对象内容
        expect({name:'viking'}).toEqual({name: 'viking'})
        // snapshot
        // npm test 后 w u 更新全部快照
        // w i 逐个更新快照
        expect(wrapper).toMatchSnapshot()
        // 对于类new Date()类型不定值
        expect(wrapper).toMatchSnapshot({
            time: expect.any(Date)
        })
    })
   
    test('should trigger the correct function callbacks', ()=>{
        const handleModifyItem = jest.fn();
        const wrapper = shallow(<PriceList handleModifyItem={handleModifyItem}/>)
        wrapper.find('.submit').simulate('click')
        expect(handleModifyItem).toHaveBeenCalledWith(1)
        expect(handelClick).toHaveBeenCalledWith(
            expexct.objectContaining({
                'action': 'click',
                'error': false0 07
            })
        )
    })

})

三 Enzyme

挂载 Enzyme

1 src/utils/testSetup.js

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({adapter: new Adapter()})

2 jest.config.js

"setupFilesAfterEnv": [
    '<rootDir>/src/utils/testSetup.js'
]

使用Enzyme

import React from 'react';
import { shallow } from 'enzyme';

let wrapper;
describe('', ()=>{
    beforeEach(()=>{
        wrapper = shallow(<TotalPrive {...props} />)
        // 打印出DOM
        console.log(wrapper.debug())
    })
    test('should render the compoent to match snapshot', ()=>{
        expect(wrapper).toMatchSnapshot()
    })
    test('test', ()=>{
        expect(wrapper.find('.income span').text()).toEqual('1000')
    })
    test('should render correct icon and price for each item', ()=>{
        const iconList = wrapper.find('list-item').first().find(Icon)
        //const icon5 = wrapper.find('list-item').at(5)
        expect(iconList.length).toEqual(3)
    })
    test('click the year item, should trigger the right status change', ()=>{
        wrapper.find('submit').sumilate('click')
        expect(wrapper.find('.years-range').first().hasClass('active')).toEqual(true)
        expect(wrapper.state('selectedYear')).toEqual(2014)
    })
})

01 容器组件 test, 确认传入了正确的数据即可,组件内部有自己的UT

 beforeEach(()=>{
    wrapper = shallow(<TotalPrive {...props} />)
})
test('should render the default layout', ()=>{
    expect(wrapper.find(PriceList).length).toEqual(1)
    expect(wrapper.find(ViewTab).props().activeTab).toEqual(LIST_VIEW)
})

02 工具函数

export const getInputValue = (selector, wrapper) =>(
    wrapper.find(selector).instance().value
)
export const setInputValue = (selector, newValue, wrapper) =>{
    wrapper.find(selector).instance().value = newValue
}

// 解耦 test 与CSS 
// <div data-test='container'> ... </div>
export const findTestWrapper = (wrapper, tag) =>{
    return wrapper.find(`[data-test="${tag}"]`)
})
const container = findTestWrapper(wrapper, 'container') 
expect(container.length).toBe(1)

四 Jest 钩子

describe('counter', ()=>{
    let counter = null;
    beforeAll(()=>{
     //全局准备
      console.log('beforeAll')
    })
    beforeEach(()=>{
       // 每次运行用例前调用
        counter = new Counter()  
    })
    afterEach(()=>{
       // 每次运行用例后调用
        counter = new Counter()  
    })
    afterAll(()=>{
        //全局清理
        console.log('afterAll')
    })

    describe('add', ()=>{
        test('addOne', ()=>{
             counter.addOne()
        })
         // test.only只执行这个用例(临时调试)
         // 上面的钩子会被触发
        test.only('addTwo', ()=>{
             counter.addTwo()
        })
    })
    
    describe('minus', ()=>{
        test('minusOne', ()=>{
             counter.minusaddOne()
        })
        test('minusTwo', ()=>{
             counter.minusTwo()
        })
    })
    
})

五 异步测试

01 测试回调

export const fetchData =(fn)=>{
    axios.get('http://www.abc.com/demo.json').then((response)=>{
      fn(resoponse.data)  
    })
}
test('', (done)=>{
    fetchData((data)=>{
        expect(data).toEqual({success: true})
        done()
    })
})

02 测试promise

export const fetchData = () => {
    return axios.get('http://www.abc.com/demo.json')
}
test('success', ()=>{
  // 注意要把promise return 出去
  return fetchData().then((response)=>{
      expect(response.data).toEqual({
        success: true
      })
  })  
})

test('404', ()=>{
 // 注意确保expect至少调用一次
  expect.assertions(1);
  return fetchData().catch((e)=>{
      expect(e.toString().indexOf('404') > -1).toBe(true)
  })
})

或者 toMatchObject

test('success', ()=>{
  // toMatchObject 包含对象
  return expect(fetchData().resolves.toMatchObject({
    data: {
        success: true
    }
  }))  
})

test('404', ()=>{
  return expect(retchData()).resolves.toThrow()  
})

或者 async await

test('success', async ()=>{
  const response = await fetchData()
  expect(response.data).toEqual({
    success: true
  })
})

test('404', async ()=>{
    // 确保expect执行1次
    expect.assertions(1)
    try{
        await fetchData()
    }catch(e){
        expect(e.toString().indexOf('404') > -1).toBe(true)
    }
})

六 Mock

01 mock function

test('runCallback', ()=>{
    const func = jest.fn();
    // func第一次执行时返回abc
    func.mockReturnValueOnce('abc')
    
    // func按顺序返回
    func.mockReturnValueOnce('abc').mockReturnValueOnce('e').mockReturnValueOnce('f')
    
    // func每次执行时返回efg
    // const func = jest.fn(()=>{return 'efg'});
    // func.mockReturnValue('efg')
    
    
    runCallback(func)
    expect(func).toBeCalled()
    runCallback(func)
    expect(func.mock.calls.length).toBe(2)
})

func.mock得值

01 mock 异步函数

demo.js

export const fetchData = ()=>{
    return axios.get('/').then(res=>res.data)
    // data:"(function(){return '123'})()"
}

demo.js同级目录下__mocks__/demo.js

export const fetchData = ()=>{
    return new Promise((resolved, reject)=>{
        resolved("(function(){return '123'})()")
    })
}

demo.test.js

jest.mock('./demo');
// jest.unmock('./demo') 取消mock
// jest.config.js 开启 automock: true 效果相同 jest.mock('./demo')

import {fetchData} from './demo';  // mock fetchData
const {getNumber} = jest.requireActual('./demo'); // unmock getNumber
test('', ()=>{
    return fetchData().then(data=>{
        expect(eval(data)).toEqual('123')
    })
})

02 mock axios 其一

demo.js

export const getData = () => {
    return axios.get('/api').then(res => res.data)
}

demo.test.js

import axios from 'axios';
jest.mock('axios')
test('getData', async ()=>{
    axios.get.mockResolvedValue({data: 'hello')
    // 只mock一次
    // axios.get.mockResolvedValueOnce({data: 'hello')
    await getData().then((data)=>{
        expect(data).toBe('hello')
    })
})

03 mock axios 其二

* 01 src/mocks/axios.js
import {testCategories, testItems} frim '../tsetData'
export default {
    get: jest.fn((url) => {
        if(url.indexOf('categories') > -1){
            return Primise.resolve({data: testCategories})
        }else if(url.indexOf('items?') > -1){
            return Primise.resolve({data: testItems})
        }else if (url.indexOf('items/') > -1){
            return Promise.resolve({data: {...testItems[0], id: 'testID'}})
        }
        
    })
}
* 02 App.test.js
import mockAxios from './__mocks__/axios'
import {testCategories, testItems} frim '../tsetData'

const waitForAsync = ()=> new Promise(resolve => setImmediate(resolve))

describe('waitForAsync 方式', ()=>{
    afterEach(()=>{
        jest.clearAllMocks()
    })
    it('', async()=>{
        const wrapper = mount(<APP />)
        expect(mockAxios.get).toHaveBeenCalledTimes(2)
        //等待setState生效
        await waitForAsync()
        const currentState = wrapper.instance().state
        expect(Objext.keys(currentState.items).length).toEqual(testItems.length)
    })
})

describe('nextTick 方式', ()=>{
    afterEach(()=>{
        jest.clearAllMocks()
    })
    it('', (done)=>{
        const wrapper = mount(<APP />)
        expect(mockAxios.get).toHaveBeenCalledTimes(2)
       
       process.nextTick(()=>{
           wrapper.update();
           const currentState = wrapper.instance().state
            expect(Objext.keys(currentState.items).length).toEqual(testItems.length)
            done()
       })
        
    })
})

04 mock timers

timer.js

export default (callback)=>{
    setTimeout(()=>{
        callback()
    },3000)
}

timer.test.js

import timer from './timer'

test('timer', (done)=>{
    timer(()=>{
        expect(1).toBe(1)
        done()
    })
})

better timer.test.js

import timer from './timer'
beforeEach(()=>{
    // useFakeTimers 放在 beforeEach 避免 advanceTimersByTime 跨test相互影响
    jest.useFakeTimers()
})

test('timer', ()=>{
    const fn = jest.fn();
    timer(fn);
    jest.runOnlyPendingTimers();// 马上执行当前timers 避免等待时间
    jest.runAllTimers() // 马上执行全部timers 比如嵌套timers
    // advanceTimersByTime 可替代上面快进时间
    jest.advanceTimersByTime(1000) // 快进1秒
    expect(fn).toHaveBeenCalledTimes(1)
})

05 mock Class

demo.js

import Util from './util'
//Util 是异常复杂的类
const demoFunction = (a,b)=>{
    const util = new Util()
    util.a(a)
    util.b(b)
    // UT不应关心util内部和返回值,只关心a b是否调用
}
expect default demoFunction

方式1 demo.test.js

jest.mock('./util')
import Util from './util'
// jest 将util转化为
// const Util = jest.fn()
// Util.a = jest.fn()
// Util.b = jest.fn()
import demoFunction from './demo';
test(''()=>{
    demoFunction()
    expect(Util).toHaveBeenCalled()
    expect(Util.mock.instances[0].a).toHaveBeenCalled()
    expect(Util.mock.instances[0].b).toHaveBeenCalled()
})

方式2 对util深层次定制mock util.js同级目录 mocks util.js

const Util = jest.fn()
Util.prototype.a = jest.fn()
Util.prototype.b = jest.fn()
expect default Util;

如不想用__mocks__文件夹形式,也可用回调

jest.mock('./util', ()=>{
    const Util = jest.fn()
    Util.prototype.a = jest.fn()
    Util.prototype.b = jest.fn()
    return Util;
})

06 mock actions promise

import {Create} from '../Create';
const history = {push:()=>{}}
const actions = {
    getEditData: jest.fn().mockReturnValue(Promise.resole({editItem: testItem}))
}

describe('', ()=>{
    it('', ()=>{
        const wrapper = mount(
        <Create actions={actions} history={history}/>
        )
        expect(actions.getEditData).toHaveBeenCalledWith(testItem.id)
    })
})

05 mock input file

    test('upload image', done => {
        const mockFile = new File(['abc'], 'abc.png', {
            type: 'image/png'
        })
        const props = {
            updatePicture: ({picture, isError})=>{
                expect(isError).toEqueal(false);
                expect(picture).toBeTruthy();
                done();
            }
        }
        const UploadComponent = mount(<Upload {...props});
        UploadComponent.find('input').simulate('change',{
            target: {},
            detaTransfer: {
                files: [mockFile]
            }
        })
    })

06 mock document event

test('after the dropdown is shown, click document should close the dropdown', ()=>{
    let eventMap = {}
    document.addEventListener = jest.fn((event, cb) => {
        eventMap[event] = cb
    })
    const wrapper = shallow(<TotalPrive {...props} />)
    wrapper.find('.dropdown-toggle').simulate('click')
    expect(wrapper.state('isOpen')).toEqual(true)
    expect(wrapper.find('.dropdown-ment').length).toEqual(1)
    
    eventMap.click({
        target: ReactDOM.findDOMNode(wrapper.instance())
    })
    expect(wrapper.state('isOpen')).toEqual(true)
    
    eventMap.click({
        target: document
    })
    expect(wrapper.state('isOpen')).toEqual(false)
})

七 UT插件

1 jsdom 模拟DOM

2 Enzyme 组件rendering

Shallow Rendering 浅render

Full Rendering 深render

Static Rendering 静态render

3 Nock 模拟HTTP

4 Sinon 函数跟踪

5 Istanbul

5 Jest

(文件路径不能有空格)

6 jest-enzyme

github.com/FormidableL…

jest.config.js

 "setupFilesAfterEnv": [
    './node_modules/jest-enzyme/lib/index.js'
]
const container = wrapper.find('[data-test="container"]')
expect(container.length).toExist()
expect(container.length).toHaveProp('title', 'dell lee')