ТОС
- Часть 1: Базовый компонент
- Часть 2: Удаление, фильтрация и передача реквизита
- Часть 3. Реорганизация нашего компонента
- Часть 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"); });
А теперь давайте запустим наш пакет!
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) 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.
Пожалуйста, поделитесь своими мыслями, вопросами, ошибками, критикой и идеями ниже!
Если вам понравилось, хлопните в ладоши и расскажите своим друзьям!
Спасибо за чтение! Быть в курсе!