使用 TDD 開發組件庫系列 --- Notification

前言

本系列是使用 TDD 開發組件庫,嘗試把 TDD 在前端落地。本系列屬於開發日記,包含了我在開發組件過程當中的思考和對 TDD 的實施,不會詳細的介紹 TDD 是什麼,怎麼使用。若是你須要瞭解 TDD 是什麼的話,請去看 《測試驅動開發》。要開發的組件所有參照 elementUI,沒有任何的開發計劃,屬於想寫哪一個組件就寫哪一個組件。ps 謹慎入坑,本文很長html

組件描述

elementUI 關於這個組件詳細的文檔 傳送門前端

需求分析

首先咱們先作的第一件事就是先分析下需求,看看這個組件都有什麼功能。把功能都列出來vue

List

  1. 能夠設置彈窗的標題
  2. 能夠設置彈窗的內容
  3. 能夠設置彈窗是否顯示關閉按鈕
  4. 點擊關閉按鈕後,能夠關閉彈窗
  5. 能夠設置關閉後的回調函數
  6. 能夠經過函數調用顯示組件

好了,咱們第一期的需求暫時是上面的幾個。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
    }
  },
複製代碼

showClose - 能夠設置彈窗是否顯示關閉按鈕

測試

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 的測試和實現都補齊

經過 notify 設置 message

測試

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;
}
複製代碼

經過 notify 設置 showClose

測試

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;
}
複製代碼
  1. 咱們建立一個 setProp 明確的寫出這個操做是更新 prop 的。這裏體現了代碼的可讀性。
  2. 咱們把設置屬性的操做都放到 updateProps 內,讓職責單一

這時候咱們須要跑下單側,看看此次的重構是否破壞了以前的邏輯(這很重要!)

咱們再仔細地看看代碼,又發現了一個問題,爲何咱們須要再 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;
}
複製代碼

跑下測試(重要!)

好了,暫時看起來代碼結構還不錯。咱們又能夠愉快的繼續寫下面的需求了

經過 notify 設置 onClose --> 關閉時的回調函數

測試

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);
}
複製代碼

未完

後續敬請期待,天天會更新一點

github

倉庫代碼 傳送門

最後求個 star ~~

相關文章
相關標籤/搜索