本系列是使用 TDD 开发组件库,尝试把 TDD 在前端落地。本系列属于开发日记,包含了我在开发组件过程当中的思考和对 TDD 的实施,不会详细的介绍 TDD 是什么,怎么使用。若是你须要了解 TDD 是什么的话,请去看 《测试驱动开发》。要开发的组件所有参照 elementUI,没有任何的开发计划,属于想写哪一个组件就写哪一个组件。ps 谨慎入坑,本文很长html
elementUI 关于这个组件详细的文档 传送门前端
首先咱们先作的第一件事就是先分析下需求,看看这个组件都有什么功能。把功能都列出来vue
好了,咱们第一期的需求暂时是上面的几个。git
这个需求列表会不断地扩充,先从简单的功能入手是 TDD 的一个技巧,随着需求不断地被实现,咱们将会对功能有更深层次地理解,可是一开始咱们并不须要考虑出全部的状况。github
会基于上面的功能 list 来一个一个的实现对应的功能api
这个需求应该是最简单的,咱们就从最简单的需求入手。先写下第一个测试浏览器
describe("Notification", () => {
it("应该有类名为 wp-button 得 div", () => {
const wrapper = shallowMount(Notification);
const result = wrapper.contains(".wp-notification");
expect(result).toBe(true);
});
});
复制代码
咱们先基于测试驱动出来一个 class 为 wp-notification 的 div, 这里实际上是存在质疑的,有没有必要经过测试来驱动出这么小的一个步骤,我这里的策略是先使用测试驱动出来,在最后的时候,使用快照功能,而后就能够把这个测试删除掉了。后续能够看到编写快照测试的逻辑。bash
写逻辑使其测试经过app
<template>
<div class="wp-notification">
</div>
</template>
复制代码
接着写下第二个测试,这个测试就是真真正正的设置弹窗显示的标题了dom
describe("props", () => {
it("title - 能够经过 title 设置标题", () => {
const wrapper = shallowMount(Notification, {
propsData: {
title: "test"
}
});
const titleContainer = wrapper.find(".wp-notification__title");
expect(titleContainer.text()).toBe("test");
});
});
复制代码
首先咱们经过设置属性 title 来控制显示的 title,接着咱们断言有一个叫作 .wp-notification__title 的 div,它的 text 内容等于咱们经过属性传入的值。
写逻辑使其测试经过
<template>
<div class="wp-notification">
<div class="wp-notification__title">
{{title}}
</div>
</div>
</template>
复制代码
export default {
props:{
title:{
type:String,
default:""
}
}
}
复制代码
好了,咱们接下来要如法炮制的把内容、关闭按钮、驱动出来。下面我会直接贴代码
it("message - 能够经过 message 设置说明文字", () => {
const message = "这是一段说明文字";
const wrapper = shallowMount(Notification, {
propsData: {
message
}
});
const container = wrapper.find(".wp-notification__message");
expect(container.text()).toBe(message);
});
复制代码
<div class="wp-notification__message">
{{ message }}
</div 复制代码
props: {
title: {
type: String,
default: ''
},
message: {
type: String,
default: ''
},
showClose: {
type: Boolean,
default: true
}
},
复制代码
it("showClose - 控制显示按钮", () => {
// 默认显示按钮
const wrapper = shallowMount(Notification);
const btnSelector = ".wp-notification__close-button";
expect(wrapper.contains(btnSelector)).toBe(true);
wrapper.setProps({
showClose: false
});
expect(wrapper.contains(btnSelector)).toBe(false);
});
复制代码
<button v-if="showClose" class="wp-notification__close-button" ></button>
复制代码
props: {
title: {
type: String,
default: ''
},
message: {
type: String,
default: ''
},
showClose: {
type: Boolean,
default: true
}
},
复制代码
it("点击关闭按钮后,应该调用传入的 onClose ", () => {
const onClose = jest.fn();
const btnSelector = ".wp-notification__close-button";
const wrapper = shallowMount(Notification, {
propsData: {
onClose
}
});
wrapper.find(btnSelector).trigger("click");
expect(onClose).toBeCalledTimes(1);
});
复制代码
咱们指望的是点击关闭按钮的时候,会调用传入的 onClose 函数
<button v-if="showClose" class="wp-notification__close-button" @click="onCloseHandler" ></button>
复制代码
咱们先给 button 添加一个 click 处理
props: {
onClose: {
type: Function,
default: () => {}
}
},
复制代码
在添加一个 onClose ,接着在点击关闭按钮后调用便可。
methods: {
onCloseHandler() {
this.onClose();
}
}
复制代码
it("notify() 调用后会把 notification 添加到 body 内", () => {
notify();
const body = document.querySelector("body");
expect(body.querySelector(".wp-notification")).toBeTruthy();
})
复制代码
咱们检测 body 内部是否能查找到 notification 做为判断条件。
ps: jest 内置了 jsdom ,因此能够在测试得时候使用 document 等浏览器 api
新建一个 index.js
// notification/index.js
import Notification from "./Notification.vue";
import Vue from "vue";
export function notify() {
const NotificationClass = Vue.extend(Notification);
const container = document.createElement("div");
document.querySelector("body").appendChild(container);
return new NotificationClass({
el: container
});
}
window.test = notify;
复制代码
这时候会发现一个问题,咱们建立得 Notification 不是经过 vue-test-utils 建立的,咱们没有办法向上面同样经过 mound 建立一个 wrapper 来快速得验证组件得结果了。咱们须要想办法依然借助 vue-test-utils 来快速验证由 notify() 建立出来得 notification 组件。
在查阅 vue-test-utils 时我发现了一个方法: createWrapper(), 经过这个咱们就能够建立出来 wrapper 对象。
咱们写个测试来测试一下: 经过 notify 设置组件得 title
it("设置 title ", () => {
const notification = notify({ title: "test" });
const wrapper = createWrapper(notification);
const titleContainer = wrapper.find(".wp-notification__title");
expect(titleContainer.text()).toBe("test");
});
复制代码
咱们经过 createWrapper 建立 wrapper 对象,接着向咱们以前测试 title 同样来测试结果
import Notification from "./Notification.vue";
import Vue from "vue";
export function notify(options = {}) {
const container = document.createElement("div");
document.querySelector("body").appendChild(container);
return createNotification(container, options);
}
function createNotification(el, options) {
const NotificationClass = Vue.extend(Notification);
const notification = new NotificationClass({ el });
notification.title = options.title;
return notification;
}
复制代码
注意我把以前建立得 vue 组件得逻辑封装到了 createNotification 内了 (随着逻辑得增长要不断得重构,保持代码得可读性,TDD 得最后一个步骤就是重构)
这里我是硬编码让 options.title 赋值给 notification.title 得。
还有一种方式是经过 Object.assign() 的方式动态的赋值传过来的全部属性,可是缺点是代码阅读性不好,当我须要查看 title 属性哪里被赋值的时候,搜索代码根本就找不到。因此我这里放弃了这种动态的写法。
至此,咱们这个测试也就经过了。
其中还有一个疑问,咱们以前已经有测试来保障设置 title 是正确的逻辑了,这里还有必要再重写一遍嘛? 我给出的答案这里是须要的,由于经过 notify() 也是暴漏给用户的 api,咱们须要验证其结果是不是正确的。只不过若是后面咱们用不到经过组件的 props 来传值实现的话,那么咱们能够删除掉以前的测试。咱们须要保证测试的惟一性,不能重复。也就是说经过测试驱动出来的测试也是容许被删除掉的。
继续咱们把 message showClose 的测试和实现都补齐
it("设置 message ", () => {
const message = "this is a message";
const wrapper = wrapNotify({ message });
const titleContainer = wrapper.find(".wp-notification__message");
expect(titleContainer.text()).toBe(message);
});
复制代码
解释下: wrapNotify(): 咱们会发现每次都须要调用 createWrapper() 来建立出对应的 wrapper 对象,为了方便后续的调用,不如直接封装一个函数。
function wrapNotify(options) {
const notification = notify(options);
return createWrapper(notification);
}
复制代码
这其实就是重构,每次写完了测试和逻辑以后,咱们都须要停下来看一看,是否是须要重构了。要注意的是测试代码也是须要维护的,因此咱们要保持代码的可读性、可维护性等。
function createNotification(el, options) {
const NotificationClass = Vue.extend(Notification);
const notification = new NotificationClass({ el });
notification.title = options.title;
notification.message = options.message;
return notification;
}
复制代码
it("设置 showClose", () => {
const wrapper = wrapNotify({ showClose: false });
const btnSelector = ".wp-notification__close-button";
expect(wrapper.contains(btnSelector)).toBe(false);
});
复制代码
function createNotification(el, options) {
const NotificationClass = Vue.extend(Notification);
const notification = new NotificationClass({ el });
notification.title = options.title;
notification.message = options.message;
notification.showClose = options.showClose;
return notification;
}
复制代码
好了,这时候咱们须要停下来看看是否须要重构了。
测试部分的代码我认为暂时还好,能够不须要重构,可是咱们看业务代码
// createNotification() 函数
notification.title = options.title;
notification.message = options.message;
notification.showClose = options.showClose;
复制代码
当初咱们是为了可读性才一个一个的写出来,可是这里随着需求逻辑的扩展,慢慢出现了坏的味道。咱们须要重构它
function createNotification(el, options) {
const NotificationClass = Vue.extend(Notification);
const notification = new NotificationClass({ el });
updateProps(notification, options);
return notification;
}
function updateProps(notification, options) {
setProp("title", options.title);
setProp("message", options.message);
setProp("showClose", options.showClose);
}
function setProp(notification, key, val) {
notification[key] = val;
}
复制代码
这时候咱们须要跑下单侧,看看此次的重构是否破坏了以前的逻辑(这很重要!)
咱们再仔细地看看代码,又发现了一个问题,为何咱们须要再 createNotification() 里面更新属性呢,这样就违反了职责单一呀。
// index.js
export function notify(options = {}) {
const container = document.createElement("div");
document.querySelector("body").appendChild(container);
const notification = createNotification(container, options);
updateProps(notification, options);
return notification;
}
function createNotification(el) {
const NotificationClass = Vue.extend(Notification);
const notification = new NotificationClass({ el });
return notification;
}
复制代码
重构后的代码,咱们把 updateProps() 提到了 notify() 内,createNotification() 只负责建立组件就行了。
跑测试(重要!)
接着咱们再看看代码还有没有要重构的部分了。
const container = document.createElement("div");
document.querySelector("body").appendChild(container);
复制代码
嗯,咱们又发现了,这其实能够放到一个函数中。
function createContainerAndAppendToView() {
const container = document.createElement("div");
document.querySelector("body").appendChild(container);
return container;
}
复制代码
嗯,这样咱们经过函数名就能够很明确的知道它的职责了。
在看看重构后的 notify()
export function notify(options = {}) {
const container = createContainerAndAppendToView();
const notification = createNotification(container);
updateProps(notification, options);
return notification;
}
复制代码
跑下测试(重要!)
好了,暂时看起来代码结构还不错。咱们又能够愉快的继续写下面的需求了
it("should onClose --> 关闭时的回调函数,关闭后应该调用回调函数", () => {
const onClose = jest.fn();
const wrapper = wrapNotify({ onClose });
const btnSelector = ".wp-notification__close-button";
wrapper.find(btnSelector).trigger("click");
expect(onClose).toBeCalledTimes(1);
});
复制代码
测试写完后,会报错,提示 btn 找不到,这是为何呢??? 能够思考一下
首先咱们要思考的是,什么会影响到 btn 找不到,只有一个影响点,那就是 options.showClose 这个属性,只有它为 false 的时候,按钮才不会显示。咱们在 Notification.vue 内不是写了 showClose 的默认值为 true 嘛,为何这里是 false 呢? 问题其实出在咱们传给 notify 的 options, 在咱们赋值 setProp 时,options 确定是没有 showClose 的。因此咱们须要给 options 一个默认值。
export function notify(options = {}) {
const container = createContainerAndAppendToView();
const notification = createNotification(container);
updateProps(notification, mergeOptions(options));
return notification;
}
function mergeOptions(options) {
return Object.assign({}, createDefaultOptions(), options);
}
function createDefaultOptions() {
return {
showClose: true
};
}
复制代码
新增了 mergeOptions() 和 createDefaultOptions() 两个函数,这里特地说明一下为何要用 createDefaultOptions 生成对象,而不是使用 const 直接在最外层定义一个配置对象。首先咱们知道 const 是不能阻止修改对象内部的属性值得。每次都建立一个全新得对象,就是为了保证这个对象是不可变的(immutable)。
好了,补齐了上面的逻辑后,测试应该只会抱怨 onClose 没有并调用了。
function updateProps(notification, options) {
setProp(notification, "title", options.title);
setProp(notification, "message", options.message);
setProp(notification, "showClose", options.showClose);
// 新增
setProp(notification, "onClose", options.onClose);
}
复制代码
后续敬请期待,天天会更新一点
仓库代码 传送门
最后求个 star ~~