Использование TDD для разработки компонентов --- Уведомление (часть 1)

внешний интерфейс

предисловие

В этой серии статей мы используем TDD для разработки библиотек компонентов и пытаемся реализовать TDD во внешнем интерфейсе. Эта серия относится к дневнику разработчиков, который включает в себя мои размышления в процессе разработки компонентов и реализации TDD, не будет подробно знакомить, что такое TDD и как его использовать. Если вам нужно понять, что такое TDD, см. раздел Разработка через тестирование. Все разрабатываемые компоненты относятся к elementUI, плана разработки нет, и он относится к тому компоненту, который вы хотите написать. ps осторожно заходите в яму, эта статья очень длинная

Описание компонента

elementUI имеет подробную документацию по этому компонентупортал

анализ спроса

Первое, что мы делаем в первую очередь, это анализируем требования и смотрим, какие функции есть у этого компонента. список функций

List

  1. Вы можете установить заголовок всплывающего окна
  2. Вы можете установить содержание поп
  3. Вы можете установить, отображает ли всплывающее окно кнопку закрытия
  4. После нажатия кнопки закрытия вы можете закрыть всплывающее окно.
  5. Вы можете установить функцию обратного вызова после закрытия
  6. Компоненты могут отображаться через вызовы функций

Что ж, на данный момент наши потребности первой фазы — это вышеперечисленные.

Этот список требований будет продолжать расширяться. Это уловка TDD, чтобы начать с простых функций. Поскольку требования постоянно реализуются, мы будем иметь более глубокое понимание функций, но нам не нужно рассматривать их все на одном этапе. начало состояние.

Реализация функции

Будет основан на приведенном выше списке функций для цельной функции реализации.

Вы можете установить заголовок всплывающего окна

Это требование должно быть самым простым, поэтому начнем с самого простого требования. Сначала напишите первый тест

тестовое задание

 describe("Notification", () => { 
  it("应该有类名为 wp-button 得 div", () => {
    const wrapper = shallowMount(Notification);
    const result = wrapper.contains(".wp-notification");
    expect(result).toBe(true);
  });
}); 

Сначала вбиваем div с классом wp-notification на основе теста, Тут собственно и есть сомнения.А нужно ли прогонять такой маленький шаг через тестирование?Моя стратегия здесь заключается в том,чтоб сначала прогнать тест,а в конце использовать функцию моментального снимка,а потом этот тест удалить.. Вы можете увидеть логику написания снэпшот-тестов позже.

Напишите логику, чтобы пройти тест

логическая реализация

<template>
    <div class="wp-notification">

    </div>
</template>

Затем записать второй тест, этот тест действительно устанавливает заголовок, отображаемый во всплывающем окне

тестовое задание

  describe("props", () => {
    it("title - 可以通过 title 设置标题", () => {
      const wrapper = shallowMount(Notification, {
        propsData: {
          title: "test"
        }
      });

      const titleContainer = wrapper.find(".wp-notification__title");
      expect(titleContainer.text()).toBe("test");
    });
  });

Сначала мы управляем отображаемым заголовком, устанавливая заголовок свойства, затем мы утверждаем, что существует div с именем .wp-notification__title, текстовое содержимое которого равно значению, которое мы передали через свойство.

Напишите логику, чтобы пройти тест

логическая реализация

<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>

Давайте сначала добавим обработчик нажатия на кнопку.

  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();
  })

Мы определяем, может ли внутренняя часть тела найти уведомление в качестве условия суждения.

ps: jest имеет встроенный jsdom, так что вы можете использовать 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;

В это время мы обнаружим проблему. Созданное нами уведомление не создается vue-test-utils. Мы не можем создать обертку через mound, чтобы быстро проверить результат компонента, как указано выше. Нам нужно найти способ по-прежнему использовать vue-test-utils для быстрой проверки компонента уведомления, созданного с помощью notify().

При просмотре vue-test-utils я нашел метод: createWrapper(), с помощью которого мы можем создавать объекты-оболочки.

Давайте напишем тест, чтобы проверить это: Установите заголовок компонента через уведомление

тестовое задание

    it("设置 title ", () => {
      const notification = notify({ title: "test" });
      const wrapper = createWrapper(notification);
      const titleContainer = wrapper.find(".wp-notification__title");
      expect(titleContainer.text()).toBe("test");
    });

Мы создаем объект-оболочку с помощью createWrapper, а затем проверяем результат, как мы тестировали 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, чтобы назначить его на уведомление.title.

Также есть способ динамически присвоить все свойства, переданные Object.assign(), но недостатком является очень плохая читаемость кода, когда мне нужно проверить, где присвоено свойство title, код поиска не может быть найден вообще. . Поэтому я отказался от этого динамического письма здесь.

Пока наш тест пройден.

Так же есть сомнения, у нас предварительно настроен тест на защиту названия, это правильная логика, а там надо его переписать Ну что? Ответ, который я даю, нужен здесь, потому что через notify() это тоже ливневка к пользовательскому апи, нам нужно проверить, правильный ли результат. Но если мы вернемся меньше, чем значение, достигнутое для прохождения через компоненты реквизита, то мы можем удалить тест до падения. Нам нужно убедиться, что уникальность теста не может быть повторена. То, что проходит тест-драйв вне теста, разрешается удалять.

Продолжаем тестировать и реализовывать сообщения 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() каждый раз, чтобы создать соответствующий объект-оболочку.Чтобы облегчить последующие вызовы, лучше напрямую инкапсулировать функцию.

    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;
}

Установите 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, явную записанную эту операцию, чтобы обновить опоры. Здесь отражает читаемость кода.
  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;
}

Пройдите тест (важно!)

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 не может быть найден, почему так? ? ? подумай об этом

Первое, о чем мы должны подумать, это то, что повлияет на то, что кнопка не будет найдена.Точка воздействия только одна, и это свойство options.showClose.Только когда оно равно false, кнопка не будет отображаться. Разве мы не записали значение showClose по умолчанию в Notification.vue как истинное, почему здесь оно ложное? Проблема на самом деле заключается в параметрах, которые мы передаем для уведомления.Когда мы назначаем setProp, параметры не должны иметь showClose. Поэтому нам нужно задать для параметров значение по умолчанию.

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 не может предотвратить изменение свойств внутри объекта value. Каждый раз, когда создается новый объект, это делается для того, чтобы объект был неизменяемым (неизменяемым).

Что ж, после выполнения приведенной выше логики тест должен жаловаться только на то, что 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);
}

Под установкой

Использование TDD для разработки компонентов --- Уведомление (ниже)

github

код складапортал

Наконец-то попросите звезду~~


  • Это проект с открытым исходным кодом нашей командыelement3
  • Библиотека компонентов внешнего интерфейса, поддерживающая vue3.