本系列是使用 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 ~~