本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-02-26
在测试中使用工厂函数有两个好处:
如果有多各测试使用相同的选项调用 shallowMount:
import { shaollowMount } from '@vue/test-utils';
function createWrapper() {
return shaollowMount(TestCompoent, {
mocks: {
$bar: {
start: jest.fn(),
fail: jest.fn(),
}
}
})
}
const wrapper = createWrapper();
在编写测试时,大多数人并不考虑代码模式,这只适用于小型测试套件,但当测试套件的体积越来越大时,这将变得非常不利。没有一个明确的模式,测试代码将难以维护,乱成一团。
通常在大型代码库中都会出现计划外的模式,起先,开发人员可能想编写一个函数作某一种功能,后来根据业务的解析或扩展,就为该函数扩展了更多的功能,需要传递更多的参数来控制更多的场景、选项。
之前提到过的 beforeEach 钩子,用来在每个测试之前重写公共变量,这种方法避免了在测试重复创建对象,工厂函数是用来避免重复的另一种模式, before each 模式会更改测试运行之前使用到的变量,而工厂函数在每次调用时都会创建新对象。如果处理得当,使用工厂函数模式的测试 才 会比使用 before each 模式的测试更容易沿用。
使用工厂函数所付出的代价是增加了代码中的抽象内容,这会使得测试让未来的开发人员更加难以理解。当未来的开发者阅读他人编写的测试时,如果不查看函数的内部实现,他们就无法得知工厂函数作了什么,就需要花费额外的时间。
考虑到这一点,测试中的重复代码也不是一无是处,如果一个测试是独立的,没有其他抽象代码,那么未来的开发者会更加容易理解。所以这也需要一种权衡,当你需要抽象代码的时候。
因此上一章节的 beforeEach 代码可以使用工厂函数改写为这样:
function createStore() {
const defaultStoreConfig = {
getters: {
displayItems: jest.fn(),
},
actions: {
fetchListData: jest.fn(() => Promise.reslove())
}
}
return new Vuex.Store(defadefaultStoreConfig);
}
如果 store 始终以相同的方式运行,这种解决办法是一个不错的选择,但是在测试中,你通常需要控制 store 的返回值,同时也有可能需要一个方法来覆盖 defadefaultStoreConfig 的某些值。
这里使用的工厂函数,返回一个新的对象,就不要担心上一章说的引用问题导致测试泄露了。
要想控制 store 的返回值,可以在 createStore 函数外面声明,用传参的方式传递给 createStore:
const items = [{}, {}, {}];
createStore(items);
const fetchListData = jest.fn();
createStore([], fetchListData);
当时上面的代码,依然有问题,就是当需要“变化”的选项越来越多,参数就会越来越多,这也就是前面所说的没有明确的模式;
所以这种情况,可以使用对象传值来解决,然后在 createStore 函数中来 merge 这些选项。
const options = {
state: {
count: 0
}
}
createStore(options);
function createStore(options) {
// ...
// ...
return new Vuex.Store(merge(defadefaultStoreConfig, options));
}
不要使用 ...
或 Object.assign()
来实现 merge,因为它们处理对象的合并遇到同名属性的时候是替换,而不是添加;
const a = {
b: {
name: 'jack',
age: 18
}
}
Object.assign(a, { b: { sex: '男' }});
// 你可能原本想将sex合并到b中,但是合并之后就会只有sex属性了,name age就没了,整个b被替换掉
// ... 解构也是一样
上面的代码同样存在一个问题,需要注意就是当使用合并的时候,你的真实意图是使用空值(专指空对象和空数组)来覆盖某一个值,但是对象和数组合并的实现,空值的时候会被忽略,不会合并到默认的选项中,类似下面这种。
const a = [1, 2];
merge(a, []); // 你可能这时候想使用的是替换,却因为合并的实现却只能变成添加,因此相当于a没有变化。
总结,使用场景下,当你使用对象传值然后合并选项的时候,传值数组改变原有数组选项一般是直接替换,传值对象改变原本对象选项一般是添加合并,传值空对象则意味着使用空对象替换原有的值。
import mergeWith from 'loadsh.mergeWith'; // npm i -D loadsh.mergeWith
function customizer(defaultValue, srcValue) {
// 如果传值的是数组 直接覆盖
if (Array.isArray(srcValue)) {
return srcValue;
}
if (srcValue instanceof Object && Object.keys(srcValue).length === 0) {
return srcValue;
}
}
// mergeWith的第三个参数用来改变合并策略
mergeWith(defaultOptions, overrideOptions, customizer);
参考 note6/#测试一个组件中的-vuex-代码示例 可以将其使用工厂函数改写为
import Vuex from 'vuex';
import mergeWith from 'loadsh.mergeWith';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import flushPromises from 'flush-promises';
import ItemList from '../ItemList.vue';
import Item from '../../components/Item.vue';
import { customizer } from './utils';
const localVue = createLocalVue();
localVue.use(Vuex);
function createStore(overrides) {
const defaultStoreConfig = {
getters: {
displayItems: jest.fn(),
},
actions: {
fetchListData: jest.fn(() => Promise.reslove())
}
}
return new Vuex.Store(mergeWith(defaultStoreConfig, overrides, customizer));
}
function createWrapper(overrides) {
const defaultOptions = {
mocks: {
$bar: {
start: jest.fn(),
finish: jest.fn(),
fail: jest.fn(),
}
},
localVue,
store: createStore()
}
return shallowMount(
ItemList, // 这里就没有必要抽象了,因为单元测试都只针对某一个组件 以及这个组件用于的默认选项
mergeWith(defaultOptions, overrides, customizer)
)
}
describe('ItemList', () => {
test('测试 displayItems getters, 是否正常获取到数据并渲染', () => {
const items = [{}, {}, {}];
const store = createStore({
getters: {
displayItems: items
}
});
const wrapper = createWrapper({ store });
const Items = wrapper.findAll(Item);
expect(Items).toHaveLength(items.length);
Items.wrappers.foeEach((wrapper, i) => {
expect(wrapper.vm.item).toBe(items[i]);
})
})
test('测试 $bar 的 start 方法是否正常被调用', () => {
const mocks = {
$bar: {
start: () => jest.fn(),
}
}
const wrapper = createWrapper({ mocks });
expect(mocks.$bar.start).toHaveBeenCalled();
})
test('测试 $bar 的 finish 方法是否正常被调用', () => {
export.assertions(1);
const mocks = {
$bar: {
finish: () => jest.fn(),
}
}
const wrapper = createWrapper({ mocks });
await flushPromises();
expect(mocks.$bar.finish).toHaveBeenCalled();
})
test('测试 Action 是否被正确分发', () => {
export.assertions(1);
const store = createStore();
store.dispatch = jest.fn(() => Promise.resolve());
const wrapper = createWrapper({ store });
expect(store.dispatch).toHaveBeenCalledWith('fetchListData', { type: 'top'});
})
test('测试 Action 抛出一个错误时,$bar 的 fail 方法是否被调用', () => {
export.assertions(1);
const mocks = {
$bar: {
fail: () => jest.fn(),
}
}
const store = createStore({
actions: {
fetchListData: jest.fn(() => Promise.reject()) // 默认是正常resolve,这里需要测试异常所以使用 reject 覆盖
}
});
const wrapper = createWrapper({ store, mocks });
await flushPromises();
expect(mocks.$bar.fail).toHaveBeenCalled();
})
})