在上一篇笔记中:Vuex 4源码学习笔记 - mapState、mapGetters、mapActions、mapMutations辅助函数原理(六)
我们通过mapState的源码了解了其工作原理,以及mapState等辅助函数并不能用在Vue3的Compositon API中。
今天我们通过单元测试来学习Vuex的工具函数源码。
首先在Vuex项目的package.json文件中,可以看到有很多的test脚本
"scripts": {
//...
"lint": "eslint src test",
"test": "npm run lint && npm run build && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e && npm run test:esm",
"test:unit": "jest --testPathIgnorePatterns test/e2e",
"test:e2e": "start-server-and-test dev http://localhost:8080 \"jest --testPathIgnorePatterns test/unit\"",
"test:ssr": "cross-env VUE_ENV=server jest --testPathIgnorePatterns test/e2e",
"test:types": "tsc -p types/test",
"test:esm": "node test/esm/esm-test.js",
"coverage": "jest --testPathIgnorePatterns test/e2e --coverage",
//...
}
今天我们主要来看其中的test:unit
,也就是单元测试
Vuex使用的单元测试框架为jest
,官网:https://jestjs.io
我们在终端中去运行一下
yarn test:unit
这里有个注意的坑,安装依赖时,最好要使用yarn来安装,不要使用npm,因为Vuex本来使用的就是yarn来安装依赖,在项目中也有yarn.lock文件来锁定依赖版本。
当使用npm安装时,@vue/devtools-api包会去安装最新的版本
当使用yarn安装时,@vue/devtools-api包会是yarn.lock的版本
@vue/devtools-api的版本变动,会导致我们yarn build构建Vuex发生报错,也算是Vuex的一个小bug,已经给Vuex提了issues了。
运行结果如下:
PASS test/unit/modules.spec.js
PASS test/unit/helpers.spec.js
PASS test/unit/store.spec.js
PASS test/unit/hot-reload.spec.js
PASS test/unit/module/module.spec.js
PASS test/unit/module/module-collection.spec.js
PASS test/unit/util.spec.js
Test Suites: 7 passed, 7 total
Tests: 102 passed, 102 total
Snapshots: 0 total
Time: 2.005 s
Ran all test suites.
今天我们就从最简单的工具函数看起,src/util.js
文件,代码量不大,不到70行代码,总共7个函数。
分别为:
find (list, f)
,返回list中符合f函数匹配到的第一个元素deepCopy (obj, cache = [])
深拷贝函数forEachValue (obj, fn)
对象的遍历函数isObject (obj)
判断是否为ObjectisPromise (val)
判断是否为Promiseassert (condition, msg)
断言函数partial (fn, arg)
闭包函数,缓存函数和参数,用来以后运行
/**
* Get the first item that pass the test
* by second argument function
*
* @param {Array} list
* @param {Function} f
* @return {*}
*/
export function find (list, f) {
return list.filter(f)[0]
}
/**
* Deep copy the given object considering circular structure.
* This function caches all nested objects and its copies.
* If it detects circular structure, use cached copy to avoid infinite loop.
*
* @param {*} obj
* @param {Array<Object>} cache
* @return {*}
*/
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
/**
* forEach for object
*/
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
export function isPromise (val) {
return val && typeof val.then === 'function'
}
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
然后我们可以在找到对应的测试文件test/unit/util.spec.js
,这个就是jest要运行的测试文件
常用的jest
测试要用到的一些函数:
describe(name, fn)
可用来描述,将一些同类的测试放在一起it(name, fn, timeout)
用来运行一条测试expect(value)
每次要测试一个值时都会使用 expect 函数.toEqual(value)
很少会单独调用 expect 。通常使用 expect 和“匹配器”函数来断言某个值。比如:toEqual(value)
等等。.toBe(value)
同上.toThrowError(error?)
同上
了解了上面的常用的测试函数,就可以很容易的看明白下面的单元测试
import { find, deepCopy, forEachValue, isObject, isPromise, assert } from '@/util'
describe('util', () => {
it('find: returns item when it was found', () => {
const list = [33, 22, 112, 222, 43]
expect(find(list, function (a) { return a % 2 === 0 })).toEqual(22)
})
it('find: returns undefined when item was not found', () => {
const list = [1, 2, 3]
expect(find(list, function (a) { return a === 9000 })).toEqual(undefined)
})
it('deepCopy: normal structure', () => {
const original = {
a: 1,
b: 'string',
c: true,
d: null,
e: undefined
}
const copy = deepCopy(original)
expect(copy).toEqual(original)
})
it('deepCopy: nested structure', () => {
const original = {
a: {
b: 1,
c: [2, 3, {
d: 4
}]
}
}
const copy = deepCopy(original)
expect(copy).toEqual(original)
})
it('deepCopy: circular structure', () => {
const original = {
a: 1
}
original.circular = original
const copy = deepCopy(original)
expect(copy).toEqual(original)
})
it('forEachValue', () => {
let number = 1
function plus (value, key) {
number += value
}
const origin = {
a: 1,
b: 3
}
forEachValue(origin, plus)
expect(number).toEqual(5)
})
it('isObject', () => {
expect(isObject(1)).toBe(false)
expect(isObject('String')).toBe(false)
expect(isObject(undefined)).toBe(false)
expect(isObject({})).toBe(true)
expect(isObject(null)).toBe(false)
expect(isObject([])).toBe(true)
expect(isObject(new Function())).toBe(false)
})
it('isPromise', () => {
const promise = new Promise(() => {}, () => {})
expect(isPromise(1)).toBe(false)
expect(isPromise(promise)).toBe(true)
expect(isPromise(new Function())).toBe(false)
})
it('assert', () => {
expect(assert.bind(null, false, 'Hello')).toThrowError('[vuex] Hello')
})
})
我们可以修改上面的测试用例,然后重新运行查看效果
yarn test:unit
比如我们修改了最后一个测试用例:
it('assert', () => {
expect(assert.bind(null, false, 'H2323')).toThrowError('[vuex] Hello')
})
输出:
FAIL test/unit/util.spec.js
● util › assert
expect(received).toThrowError(expected)
Expected substring: "[vuex] Hello"
Received message: "[vuex] H2323"
可以发现测试没有通过。
通过这些测试用例,我们可以更加清楚的知道每个工具函数的作用和使用方式。对代码有着更深的理解。
一起学习更多前端知识,微信搜索【小帅的编程笔记】,每天更新