使用 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 ~~

相关文章
相关标签/搜索