ТОС

  1. Часть 1: Базовый компонент
  2. Часть 2: Удаление, фильтрация и передача реквизита
  3. Часть 3. Реорганизация нашего компонента
  4. Часть 4: Подключение к серверной части и тестирование запросов Ajax

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

Прежде чем мы запачкаем руки, убедитесь, что вы используете версию Unite.js до 0.0.4 или выше. В этой версии внесены некоторые изменения, которые довольно интересны и позволят нам провести хороший рефакторинг (например, извлечь глобальные помощники в специальный файл).

Прежде всего, давайте рассмотрим некоторые из наших методов, а затем перейдем к оптимизации наших тестов.

Метод save()

В настоящее время наш save() метод выглядит так:

save() {
  let note = this.note;
  if(note.id !== "") {
    this.notes = this.notes.map(item => {
      if(item.id === note.id) {
        return note;
      }
      return item;
    });
  } else {
    note.id = (new Date).getTime();
    this.notes.push(note);
  }
},

Это довольно грязно !! Первое, что я чувствую, это то, что этот метод делает слишком много:

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

Это много обязанностей! Мы можем разбить это на 3 метода:

  • save(): будет отвечать за определение необходимости добавления или обновления примечания
  • add(): будет отвечать за добавление новой заметки в список
  • update(): будет отвечать за обновление существующей заметки

Имея это в виду, наш метод save теперь выглядит следующим образом:

save() {
  (this.note.id) ? this.update(this.note) : this.add(this.note);
},
add(note) {
  note.id = (new Date).getTime();
  this.notes.push(note);
},
update(note) {
  this.notes = this.notes.map(item => {
    if(item.id === note.id) {
      return note;
    }
    return item;
  });
},

На мой взгляд, это намного чище! Запускаем npm run test получаем зеленый цвет! Здорово!

Теперь давайте посмотрим на цикл сопоставления заметок из метода update. По сути, для каждого элемента в массиве заметок мы сравниваем, равен ли идентификатор элемента идентификатору заметки. В таком случае мы возвращаем записку, в противном случае - возвращаем товар.

JavaScript позволяет нам делать это в одной строке:

item.id === note.id ? note : item

Итак, добавив это в код, мы получим:

update(note) {
  this.notes = this.notes.map(item => {
    return item.id === note.id ? note : item
  });
},

Теперь, поскольку у get есть однострочная функция, мы можем использовать синтаксис стрелки и написать:

update(note) {
  this.notes = this.notes.map(
    item => item.id === note.id ? note : item
  );
},

Это немного чище, и если мы запустим наши тесты, мы станем зелеными.

Отфильтрованное вычисляемое свойство

Наше вычисленное свойство filtered() использует цикл фильтра для поиска заметок, которые включают вводимый текст с его заголовком или телом. Как мы видим ниже, это делается одной строкой:

computed: {
  filtered() {
    return this.notes.filter(note => {
      return (
        note.title.includes(this.filter)
        || note.body.includes(this.filter)
      );
    });
  }
},

Итак, опять же, используя синтаксис стрелки, мы можем переписать это как:

computed: {
  filtered() {
    return this.notes.filter(
      note => note.title.includes(this.filter) || note.body.includes(this.filter)
    );
  }
},

Запустив наш тест, мы стали зелеными.

Имя опоры данных

В настоящее время наши заметки передаются через наш компонент с использованием имени свойства date. Лично мне это кажется слишком общим, и я предпочитаю что-нибудь более информативное. Я пойду с notesData. Начнем с рефакторинга нашего it accepts notes as props теста:

/** @test */
Unite.test("It accepts notes as props", () => {
    let notesData = [
        {id: 1, title: "Note 1", body: "Note body" },
        {id: 2, title: "Note 2", body: "Note body" }
    ];
    wrapper = Unite.$mount($component, {
        propsData: { notesData }
    });
    let notes = wrapper.find(".notes");
    expect(notes.vnode.children.length).toEqual(2);
    expect(notes.html()).toContain("Note 1");
});

А теперь давайте запустим наш пакет!

  1. npm run test
1) annotate.tests.vue @ It accepts notes as props => line 112
expect(received).toEqual(expected)
Expected value to equal:
  2
Received:
  0

Эта ошибка возникает из-за expect(notes.vnode.children.length).toEqual(2). Это означает, что мы ожидали, что две ноты пройдут через подпорку, но не увидели ничего. Этого и следовало ожидать, поскольку теперь мы используем опору notesData вместо data. Давайте изменим это:

props: {
  notesData: {
    default() {
      return [];
    }
  }
},
created() {
  this.notes = this.notesData;
},

Теперь, если мы запустим npm run test, мы получим зеленый цвет! Молодец!

Кнопки и действия

В нашем компоненте есть три кнопки действий:

  • Создайте
  • спасти
  • Удалить

Меня беспокоит то, что из трех кнопок только кнопка сохранения на самом деле является кнопкой. Обе кнопки создания и удаления являются div. Сделаем из них все пуговицы! Для этого нам потребуется провести рефакторинг наших тестов.

На it sees the form to create/edit notes мы ожидаем найти button элементов (это кнопка сохранения). Давайте проясним это, взяв вместо этого button.save:

Unite.test("It sees the form to create/edit notes", () => {
  let form = wrapper.find(".form");
  expect(form.find("input[name=id]").exists()).toBe(true);
  expect(form.find("input[name=title]").exists()).toBe(true);
  expect(form.find("textarea[name=body]").exists()).toBe(true);
  expect(form.find("button.save").exists()).toBe(true);
});

Теперь нам просто нужно внести это изменение в it adds a new note when the button is clicked и it updates a note object with the input data, и если мы запустим наши тесты:

1) annotate.tests.vue @ It sees the form to create/edit notes => line 27
expect(received).toBe(expected)
Expected value to be (using ===):
  true
Received:
  false

это означает, что мы не смогли найти наш button.save. В нашем шаблоне давайте добавим к кнопке класс save:

<template>
  ...
  <textarea name="body" v-model="note.body"></textarea>
  <button @click="save" class="save">Save</button>
  <div class="destroy" @click="destroy">Delete</div>
  ...
</template>

Затем нам нужно сделать как кнопку создания, так и кнопку удаления фактическими кнопками. Кроме того, класс кнопки создания получил имя .new-note. Хотя это нормально, но я думаю, что было бы более логично, если бы он назывался просто .create. Давайте изменим it shows the blank form when the new button is clicked и it destroys an existing note, чтобы искать кнопки:

/** @test */
Unite.test("It shows the blank form when the new button is clicked", () => {
  ...
  wrapper.find("button.create").trigger("click");
  ...
});
Unite.test("It destroys an existing note", () => {
  ...
  wrapper.find("button.destroy").trigger("click");
  ...
});

Если мы запустим наши тесты:

1) annotate.tests.vue @ It shows the blank form when the new button is clicked
Error: [vue-test-utils]: find did not return button.create, cannot call trigger() on empty Wrapper
2) annotate.tests.vue @ It destroys an existing note
Error: [vue-test-utils]: find did not return button.destroy, cannot call trigger() on empty Wrapper

Как мы и ожидали, элементов найти не удалось. Давайте изменим наш шаблон:

<template>
  <div class="annotate">
    <button class="create" @click="create">Add new note</button>
    ...
    <div class="form">
      ...
      <button class="destroy" @click="destroy">Delete</button>
    </div>
  </div>
</template>

Выполняя наши тесты, мы снова в зеленой зоне!

noteItem переменная в v-for

Это быстро! Я нахожу noteItem немного странным. Давайте изменим его на item, чтобы было немного чище:

<template>
  ...
  <ul class="notes">
    <li class="note" v-for="item in filtered" :key="item.id" v-text="item.title" @click="edit(item)"></li>
  </ul>
  ...
</template>

Выполняя наш тест, мы все еще на зеленом.

На данный момент наш компонент выглядит довольно хорошо! Хорошая работа! Теперь давайте немного сосредоточимся на наших тестах и ​​попробуем их очистить.

Рефакторинг наших тестов

Просматривая наши тесты, мы видим, что мы постоянно создаем кучу заметок, предпринимаем какие-то действия, а затем делаем утверждения. Более того, в нашем it accepts notes as props нам нужно повторно смонтировать компонент, чтобы передавать заметки в качестве свойств.

Хороший способ исправить это - просто передать props в нашу Unite.beforeEachTest функцию:

Unite.beforeEachTest(() => {
  let notesData = [
    {id: 1, title: "Note 1", body: "Note body" },
    {id: 2, title: "Note 2", body: "Note body" }
  ];
  wrapper = Unite.$mount($component, {
    propsData: { notesData }
  });
});

Теперь, если мы запустим наш тест, мы получим несколько ошибок:

тестов: 9 | утверждений: 14 | ошибок: 7

Давайте их исправим!

  1. Он отображает все заметки
1) annotate.tests.vue @ It displays all the notes => line 24
expect(received).toEqual(expected)
Expected value to equal:
  2
Received:
  4

Поскольку мы уже добавили две заметки в качестве реквизита, мы получаем две дополнительные заметки, чем ожидали. Удалим два addNote вызова:

/** @test */
Unite.test("It displays all the notes", () => {
  let notes = wrapper.find(".notes");
  expect(notes.vnode.children.length).toEqual(2);
});

npm run test ~ ›тестов: 9 | утверждений: 14 | ошибок: 6

2. При нажатии кнопки добавляется новая заметка.

1) annotate.tests.vue @ It adds a new note when the button is clicked => line 39
expect(received).toBe(expected)
Expected value to be (using ===):
  true
Received:
  false

В этом тесте перед нажатием кнопки save ожидалось, что элемент .notes будет пустым. После нажатия мы ожидаем увидеть одного ребенка. Поскольку мы передали два свойства, элемент содержит двух дочерних элементов и будет содержать три после щелчка. Наконец, когда мы делаем наше утверждение о новом элементе заметки, мы получаем его, используя wrapper.find(".note:first-child"). Поскольку новая заметка будет добавлена ​​в конец списка, мы должны получить .note:last-child. Давайте исправим это:

/** @test */
Unite.test("It adds a new note when the button is clicked", () => {
  type("input[name=title]", "New Note");
  type("textarea[name=body]", "New note body");
  expect(wrapper.find(".notes").vnode.children.length).toEqual(2);
  
  wrapper.find("button.save").trigger("click");
  
  expect(wrapper.find(".notes").vnode.children.length).toEqual(3);
  expect(wrapper.find(".note:last-child").text()).toEqual("New Note");
});

Итак, запускаем тест: тестов: 9 | утверждений: 16 | ошибок: 5

3. Он показывает выбранную заметку в форме.

1) annotate.tests.vue @ It shows the clicked note on the form => line 54
expect(received).toEqual(expected)
Expected value to equal:
  "1511317965226"
Received:
  "1"

Эта ошибка возникает, когда мы щелкаем .note:first-child и пытаемся подтвердить, что note id равен идентификатору в форме. Чтобы быстро исправить это, вместо этого нажмите .note:last-child:

/** @test */
Unite.test("It shows the clicked note on the form", () => {
 let note = addNote("Note 1", "Note's body");
 
 wrapper.find(".note:last-child").trigger("click");
 
 let form = wrapper.find(".form");
 expect(form.find("input[name=id]").element.value).toEqual(String(note.id));
 expect(form.find("input[name=title]").element.value).toEqual(note.title);
 expect(form.find("textarea[name=body]").element.value).toEqual(note.body);
});

Выполняем наши тесты: тестов: 9 | утверждений: 18 | ошибок: 4

4. Обновляет объект заметки входными данными.

1) annotate.tests.vue @ It updates a note object with the input data => line 69
expect(received).toEqual(expected)
Expected value to equal:
  3
Received:
  5

Как и в случае с нашей первой ошибкой, здесь мы ожидаем увидеть три заметки, но, поскольку мы добавили две заметки в качестве реквизита, мы видим пять. Если мы удалим три addNote вызова и изменим ожидаемое значение на 2, мы должны это исправить:

/** @test */
Unite.test("It updates a note object with the input data", () => {
  wrapper.find(".note:nth-child(2)").trigger("click");
  type("input[name=title]", "New Title");
  wrapper.find("button.save").trigger("click");
  expect(wrapper.vm.notes.length).toEqual(2);
  expect(wrapper.find(".note:nth-child(2)").text()).toEqual("New Title");
});

npm run test ~ ›тестов: 9 | утверждений: 19 | ошибок: 3

Хорошая работа =)

5. При нажатии новой кнопки отображается пустая форма.

1) annotate.tests.vue @ It shows the blank form when the new button is clicked => line 74
expect(received).toEqual(expected)
Expected value to equal:
  "Note Title"
Received:
  "Note 1"

Эта ошибка возникает, когда мы добавляем заметку и щелкаем по первому дочернему элементу. Если мы удалим вызов addNote и ожидаем, что заголовок будет Note 1, мы должны исправить проблему:

/** @test */
Unite.test("It shows the blank form when the new button is clicked", () => {
 wrapper.find(".note:first-child").trigger("click");
 expect(wrapper.find("input[name=title]").element.value).toEqual("Note 1");
 wrapper.find("button.create").trigger("click");
 expect(wrapper.find("input[name=id]").element.value).toEqual("");
 expect(wrapper.find("input[name=title]").element.value).toEqual("");
 expect(wrapper.find("textarea[name=body]").element.value).toEqual("");
});

Запуск тестов: тестов: 9 | утверждений: 22 | ошибок: 2

Почти готово!

6. Уничтожает существующую заметку.

1) annotate.tests.vue @ It destroys an existing note => line 89
expect(received).toEqual(expected)
Expected value to equal:
  2
Received:
  4

Опять же, мы получаем две лишние заметки! Давайте удалим addNote вызовы и исправим два ожидания в отношении заголовков:

/** @test */
Unite.test("It destroys an existing note", () => {
  wrapper.find(".note:first-child").trigger("click");
  let notes = wrapper.find(".notes");
  expect(notes.vnode.children.length).toEqual(2);
  wrapper.find("button.destroy").trigger("click");
  notes = wrapper.find(".notes");
  expect(notes.vnode.children.length).toEqual(1);
  expect(notes.html()).toContain("Note 2");
  expect(notes.html()).not.toContain("Note 1");
});

npm run test ~ ›тестов: 9 | утверждений: 25 | ошибок: 1

И, наконец, последний!

7. Он фильтрует существующие заметки.

1) annotate.tests.vue @ It filters the existing notes => line 121
expect(received).toEqual(expected)
Expected value to equal:
  3
Received:
  5

То же, что и предыдущий! Но на этот раз, поскольку мы хотим отфильтровать заметки, оставим вызовы на addNote и изменим ожидаемое значение с 3 на 5.

/** @test */
Unite.test("It filters the existing notes", () => {
  addNote("About TDD", "It is awesome!");
  addNote("Grocery list", "Melons, apples, cake");
  addNote("Party list", "Invitations, balloons, cake");
  let filter = wrapper.find("input[name=filter]");
  expect(filter.element.value).toEqual("");
  expect(wrapper.find(".notes").vnode.children.length).toEqual(5);
  type("input[name=filter]", "cake");
  expect(wrapper.find(".notes").vnode.children.length).toEqual(2);
  type("input[name=filter]", "TDD");
  expect(wrapper.find(".notes").vnode.children.length).toEqual(1);
  type("input[name=filter]", "RANDOM");
  expect(wrapper.find(".notes").vnode.children.length).toEqual(0);
});

И вот мы снова вернулись к зеленому!

На этом этапе it accepts notes as props тест является излишним, поскольку мы уже тестируем его с помощью теста it displays all the notes. Так что мы можем это удалить!

Последнее, что мы могли бы сделать, чтобы очистить наш код, - это использовать test помощники Unite.js. Этот метод является псевдонимом метода Unite.test(), который мы использовали.

Извлечение глобальных помощников

Чтобы завершить рефакторинг, давайте посмотрим, как мы можем извлечь несколько глобальных помощников и сделать их доступными для всех наборов тестов!

Во-первых, давайте создадим ./helpers.js файл в корне нашего проекта и переместим наш помощник type:

// ./helpers.js
/** @helper It types into an input */
let type = (selector, value) => {
  let node = wrapper.find(selector);
  node.element.value = value;
  node.trigger("input");
};

Теперь в наш ./unite.config.js файл добавим новый элемент:

// ./unite.config.js
module.exports = {
  path: "./src",
  setup: [ "./helpers.js" ]
}

Если мы запустим наши тесты, мы все равно будем зеленым! Потрясающий!

Если вы хотите, вы можете добавить других помощников, таких как click (как предложил Джонатан Гравуа! Спасибо! =)), Которые сделают ваш код более читаемым! Вот все, что я добавил:

// ./helpers.js

/** @helper It types into an input */
let type = (selector, value) => {
  let node = wrapper.find(selector);
  node.element.value = value;
  node.trigger("input");
};
/** @helper It clicks on an element */
let click = (selector) => {
  wrapper.find(selector).trigger("click");
};
/** @helper It counts the element's children */
let countChildren = (selector) => {
  return wrapper.find(selector).vnode.children.length;
};
/** @helper It returns the element */
let element = (selector) => {
  return wrapper.find(selector).element;
};

Заключительный компонент

Если хотите, посмотрите файл helpers.js здесь!

Выводы

В этой части мы прошли процесс рефакторинга нашего компонента Annotate с помощью наших тестов. В нашем исходном компоненте было 240 строк, а теперь менее 200. Более того, мы сделали код более последовательным и лаконичным. Для меня это большое улучшение.

В следующей статье давайте проверим наш способ подключения к серверной части через вызовы ajax.

Пожалуйста, поделитесь своими мыслями, вопросами, ошибками, критикой и идеями ниже!

Если вам понравилось, хлопните в ладоши и расскажите своим друзьям!

Спасибо за чтение! Быть в курсе!