Уверенность в каждой строке кода: использование Jest для модульного тестирования в скрипте Google Apps
Введение 📝
Так так так! Посмотрите, кто вернулся на очередную сессию «Волшебное плетение с кодом»! 😄 Сегодня мы с головой погружаемся в волшебный мир модульного тестирования. Вы можете задаться вопросом: «Почему такая суета вокруг модульного тестирования?» 🤔
Модульное тестирование, друзья мои, — безмолвный герой разработки программного обеспечения. Представьте себе это как мини-пит-стоп, где ваш код тщательно проверяется, прежде чем он отправится в путь. Автоматические тесты гарантируют, что наш код делает именно то, что мы от него ожидаем. Выгоды?
- Заставляет нас глубоко задуматься об архитектуре нашего приложения, словно философ размышляет о смысле жизни 🧐
- Заставляет нас уточнить наши ожидания от функций и классов.
- Гарантирует, что по мере того, как мы переделываем и обновляем наш код, мы по незнанию не принимаем ошибок, как вечеринка-сюрприз, которой вы не хотели 🐛
Сегодня мы рассмотрим подход разработки через тестирование (TDD). Правильно, сначала мы напишем наши тесты, а затем наш код. Это как есть десерт перед обедом, нетрадиционный, но сытный! 😋 TDD может показаться вывернутым наизнанку, но у него есть причина: он обеспечивает наилучшие результаты и лучший код.
Помните, модульные тесты — это не дополнительная начинка для вашей пиццы. Они такие же неотъемлемые, как сыр 🧀 В идеальном мире мы бы использовали это с самого начала. Но эй, это не серия TDD, так что мы пристегнемся и сделаем это прямо сейчас!
Вы можете найти полный исходный код в ветке part-05
в репозитории Github. Вы также можете поэкспериментировать с Emojibar в этой демонстрационной таблице. Я буду держать его в курсе каждой новой записи в блоге.
Представляем функцию «Последние использованные эмодзи» 😎
Наш Emojibar уже выглядит довольно шикарно. Теперь давайте добавим вишенку сверху: последний использованный раздел смайликов в нижнем колонтитуле. Вот как мы хотим, чтобы это работало:
- Если у нас нет последних использованных эмодзи и пользователь выбирает 🫣, мы отображаем 🫣
- Если у пользователя есть 2 последних использованных смайлика 🤕 и 🤮, и пользователь выбирает новый смайлик 😷, то последний смайлик перемещается в начало списка, и список становится 😷, 🤕 и 🤮
- Если у пользователя есть 😜, 😝, 😬 и 😪 в качестве последних использованных смайликов, и пользователь выбирает 😪, то он перемещается в начало списка, и список становится 😪, 😜, 😝 и 😬
- Если у пользователя есть полный список из 10 смайлов 🫠, 😉, 😊, 😇, 🥰, 😍, 🤩, 😘, 😗 и 😚, и пользователь выбирает новый смайлик 🐻, то последний выбранный смайлик перемещается в начало списка. список, и последний смайлик выскакивает из списка; и список становится 🐻, 🫠, 😉, 😊, 😇, 🥰, 😍, 🤩, 😘 и 😗
И это именно то, что мы будем тестировать.
Установка и настройка Jest 🛠️
Ах, Джест! Наш верный помощник в сегодняшних приключениях. Jest похож на тот швейцарский армейский нож для тестирования фреймворков, который обрабатывает все, что вы можете ему бросить. 😎 Он популярен, надежен, и изучение его означает, что вы можете легко перейти на другие фреймворки, если вам когда-нибудь понадобится.
Во-первых, давайте установим типы Jest и Jest (для этого сладкого, сладкого автозаполнения в нашей IDE) с помощью:
npm i -D jest @types/jest
Создайте папку с именем __tests__
на корневом уровне. Внутри него создайте файл с именем first.test.js
. Вставьте в него следующий код:
/* eslint-env jest */ describe('Making sure Jest works', () => { it('2 + 3 = 5', () => { expect(2 + 3).toBe(5); }); });
Давайте разгадаем тайну этого кода:
- Файл должен иметь расширение
test.js
, чтобы Jest знал, как его запустить (это все равно, что использовать Bat-Signal для наших тестов 🦇). describe()
объединяет наши тесты вместе, используя описательное сообщение и функцию обратного вызова.it()
принимает строку, описывающую ожидаемое поведение и функцию обратного вызова.expect(2 + 3).toBe(5)
запускает фактический тест.- У вас может быть несколько вызовов
describe()
с несколькими вызовамиit()
иexpect()
в их обратных вызовах. - Комментарий
/* eslint-env jest */
информирует ESLint о том, что мы находимся в среде Jest, поэтому он не будет волноваться.
Затем перейдите к своему package.json
и добавьте следующую строку к вашему свойству scripts
:
{ scripts: { + test: "NODE_OPTIONS='--experimental-vm-modules' jest" } }
Это сообщает нашему пакету, что запуск скрипта test
означает, что мы тестируем Jest. Нам также нужно установить параметр экспериментальных модулей vm в Node, чтобы Jest хорошо работал с модулями ES6.
Сохраните изменения и в терминале выполните:
npm run test
Или, если вы предпочитаете сокращение, npm test
или даже просто npm t
— эти трое как братья и сестры, все они делают одно и то же.
Ваш вывод должен выглядеть примерно так:
> [email protected] test > jest PASS __tests__/first.test.js Making sure Jest works ✓ 2 + 3 = 5 (2 ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 0.297 s, estimated 1 s Ran all test suites.
Ну вы бы посмотрели на это! У нас есть пройденный тест, что означает, что наш Jest настроен правильно. Но это не какой-то приз за участие 🏆 — этот тест пока не проверяет наш код. Итак, давайте наденем наши игровые лица и начнем тестировать наши последние использованные смайлики.
Создание функциональности последних использованных эмодзи с помощью TDD 🏗️
Давайте создадим файл с именем LastUsed.class.js
в нашей исходной папке и заполним его следующим кодом:
const LastUsed = (function () { const _maxLength = new WeakMap(); const _lastUsed = new WeakMap(); class LastUsed { constructor(maxLength = 10) { _lastUsed.set(this, []); _maxLength.set(this, maxLength); } list() { return _lastUsed.get(this); } add() { return this; } } return LastUsed; })(); export default LastUsed;
Итак, только что созданный файл LastUsed.class.js
определяет класс, который мы будем использовать для управления последними использованными эмодзи. У нас есть два метода: add
и list
, которые будут использоваться для добавления и извлечения последних использованных эмодзи соответственно.
Теперь давайте сделаем глубокий вдох и попрощаемся с нашим файлом first.test.js
. Это сослужило нам хорошую службу, но пришло время повысить уровень нашей тестовой игры. Создайте новый файл в папке __tests__
и назовите его last-used.test.js
. Здесь мы определим наши тесты:
/* eslint-env jest */ import LastUsed from '../src/LastUsed.class'; describe('Last Used Emojis', () => { it('Adding emoji to an empty list', () => { const lastUsed = new LastUsed(); lastUsed.add('🫣'); const expectedResult = ['🫣']; expect(lastUsed.list()).toEqual(expectedResult); }); it('Adding a new emoji to a list that is not full', () => { const lastUsed = new LastUsed(); lastUsed.add('🤮').add('🤕').add('😷'); const expectedResult = ['😷', '🤕', '🤮']; expect(lastUsed.list()).toEqual(expectedResult); }); it('Reusing an emoji that is already in the list', () => { const lastUsed = new LastUsed(); lastUsed.add('😪').add('😬').add('😝').add('😜').add('😪'); const expectedResult = ['😪', '😜', '😝', '😬']; expect(lastUsed.list()).toEqual(expectedResult); }); it('Adding a new emoji when the list is full', () => { const lastUsed = new LastUsed(); const list = [ '😚', '😗', '😘', '🤩', '😍', '🥰', '😇', '😊', '😉', '🫠' ]; list.forEach(lastUsed.add); lastUsed.add('🐻'); const expectedResult = ['🐻', '😚', '😗', '😘', '🤩', '😍', '🥰', '😇', '😊', '😉']; expect(lastUsed.list()).toEqual(expectedResult); }); });
Помните те правила, которые мы изложили на простом английском языке? Что ж, мы перевели их в модульные тесты. Здесь я использую метод toEqual()
, так как он полезен, когда нам нужно проверить глубокое равенство объектов.
Когда вы снова запустите npm t
, вы увидите, что тесты не пройдены:
FAIL __tests__/last-used.test.js Used Emojis ✕ Adding emoji to an empty list (7 ms) ✕ Adding a new emoji to a list that is not full (1 ms) ✕ Reusing an emoji that is already in the list (1 ms) ✕ Adding a new emoji when the list is full (1 ms) ● Last Used Emojis › Adding emoji to an empty list expect(received).toEqual(expected) // deep equality - Expected - 3 + Received + 1 - Array [ - "🫣", - ] + Array [] 7 | lastUsed.add('🫣'); 8 | const expectedResult = ['🫣']; > 9 | expect(lastUsed.list()).toEqual(expectedResult); | ^ 10 | }); 11 | 12 | it('Adding a new emoji to a list that is not full', () => { at Object.toEqual (__tests__/last-used.test.js:9:29) ... Test Suites: 1 failed, 1 total Tests: 4 failed, 4 total Snapshots: 0 total Time: 0.32 s, estimated 1 s Ran all test suites
Изначально провалы тестов в нашем классе не должны вызывать никакой тревоги. Нам еще предстоит написать какой-либо код в нашем классе, отсюда и сбои. Это обычное явление в разработке через тестирование (TDD), где рабочий процесс подразделяется на три этапа — красный, зеленый, рефакторинг.
«Красный» этап начинается с написания неудачных тестов, что мы сейчас и наблюдаем. Затем следует «Зеленый» этап, на котором мы разрабатываем код, отвечающий требованиям тестов, что приводит к их прохождению. Наконец, на этапе «Рефакторинг» мы реструктурируем наш код, чтобы повысить его производительность, удобочитаемость и простоту, постоянно поддерживая тесты в состоянии прохождения.
Теперь пришло время начать кодирование. Мы начнем с изменения метода add()
класса LastUsed
. Теперь этот метод будет принимать параметр emoji
. Мы получим список смайликов и воспользуемся методом unshift()
, чтобы добавить новые смайлики в начало массива _lastUsed
. Обновленный метод add()
выглядит так:
- add() { + add(emoji) { + const emojis = this.list(); + emojis.unshift(emoji); + return this; }
После этого изменения мы проведем тесты еще раз. Теперь мы видим улучшение, так как половина наших тестов проходит:
FAIL __tests__/last-used.test.js Last Used Emojis ✓ Adding emoji to an empty list (3 ms) ✓ Adding a new emoji to a list that is not full (1 ms) ✕ Reusing an emoji that is already in the list (6 ms) ✕ Adding a new emoji when the list is full
Это показывает прогресс, но два теста все еще не пройдены. Чтобы исправить это, мы дополнительно изменим метод add()
:
add(emoji) { const emojis = this.list(); const index = emojis.findIndex((item) => item === emoji); if (-1 !== index) emojis.splice(index, 1); emojis.unshift(emoji); if (emojis.length > _maxLength.get(this)) emojis.length = _maxLength.get(this); return this; }
Теперь, когда мы повторно запускаем наши тесты, все они проходят:
PASS __tests__/last-used.test.js Last Used Emojis ✓ Adding emoji to an empty list (2 ms) ✓ Adding a new emoji to a list that is not full ✓ Reusing an emoji that is already in the list (1 ms) ✓ Adding a new emoji when the list is full Test Suites: 1 passed, 1 total Tests: 4 passed, 4 total Snapshots: 0 total Time: 0.283 s, estimated 1 s Ran all test suites.
Теперь у нас есть полностью функциональный класс!
Модульные тесты также следует запускать с помощью git-хука pre-commit. Как и в предыдущем посте, давайте воспользуемся Husky 🐶, чтобы добавить его туда вот так:
$ npx husky add .husky/pre-commit "npm t" husky - updated .husky/pre-commit
Теперь, когда вы коммитите свой код, вы будете автоматически запускать линтинг, красивую печать и модульное тестирование один за другим, и если они не пройдут, то коммит не пройдет, и вы будете знать, что вам есть что исправить. Аккуратный!
Следующий шаг — интегрировать его в Emojibar.
Для этой интеграции нам нужно выполнить несколько задач:
- Добавление смайликов в наш класс
LastUsed
при нажатии на них - Визуализация смайликов в нижнем колонтитуле
last-used
Кроме того, обратите внимание, что я переименовал div#recent
в div#last-used
в файлах HTML и CSS, чтобы сделать его более явным. Прежде чем мы начнем интеграцию, обязательно запустите npm run dev
и npm run build:css:watch
параллельно, чтобы запустить сервер разработки и Tailwind CSS соответственно.
Первая часть интеграции включает в себя отслеживание последних использованных смайликов.
Конечно, давайте продолжим процесс интеграции.
Начнем с улучшения файла copyToClipboard.js
. Мы импортируем класс LastUsed
и создадим его экземпляр. Затем всякий раз, когда будет выбран смайлик, мы добавим его в экземпляр LastUsed
. Модификация copyToClipboard.js
выглядит так:
import Toastify from 'toastify-js'; import 'toastify-js/src/toastify.css'; + import LastUsed from './LastUsed.class'; + const lastUsed = new LastUsed(); export default function copyToClipboard(target) { const selectedEmoji = target.dataset.emoji; navigator.clipboard.writeText(selectedEmoji); + lastUsed.add(selectedEmoji); // ... }
Это дополнение эффективно, но его полезность ограничена, если мы не отображаем последние использованные смайлики. Давайте приступим к этому.
Функция showEmojis()
в файле search.js
похожа на то, что нам нужно для рендеринга последних использованных эмодзи. Таким образом, мы можем преобразовать его в повторно используемую функцию. Рефакторинговая функция renderEmojis()
принимает в качестве параметров массив эмодзи и строку селектора CSS. Функция выглядит следующим образом:
// /src/renderEmojis.js export default function renderEmojis(emojiAr, targetSelector) { const html = '<div class="emoji-row">' + (emojiAr .map( (emojiObj, i) => /* html */ ` <span class="m-[8px] cursor-pointer display hover:rotate-[405deg] hover:scale-150 transition-all duration-500 inline-grid emoji" title="${emojiObj.name}" data-emoji="${emojiObj.emoji}" data-emoji-name="${emojiObj.name}" >${emojiObj.emoji}</span>${0 === (i + 1) % 5 ? '<br />' : ''}` ) .join('') + '</div>'); document.querySelector(targetSelector).innerHTML = html; }
Теперь у нас есть функция, которая может отображать смайлики в указанную цель HTML. Давайте воспользуемся этой новой функцией в файле copyToClipboard.js
, создав другую функцию, renderLastUsed()
. Эта функция вызывает renderEmojis()
с последними использованными эмодзи и целевой строкой селектора CSS:
import Toastify from 'toastify-js'; import 'toastify-js/src/toastify.css'; + import EMOJIS from 'unicode-emoji-json/data-by-emoji.json' + import renderEmojis from './renderEmojis'; import LastUsed from './LastUsed.class'; const lastUsed = new LastUsed(); export default function copyToClipboard(target) { const selectedEmoji = target.dataset.emoji; navigator.clipboard.writeText(selectedEmoji); - lastUsed.add(selectedEmoji); + renderLastUsed(lastUsed.add(selectedEmoji)); // ... } + function renderLastUsed(lastUsed) { + const lastUsedEmojis = lastUsed.list(); + + const emojiAr = lastUsedEmojis.reduce((acc, item) => { + if (EMOJIS[item]) { + const emoji = EMOJIS[item]; + emoji.emoji = item; + return [...acc, emoji]; + } + return acc; + }, []); + + renderEmojis(emojiAr, 'div#last-used'); + }
Теперь при выборе смайлика он добавляется в экземпляр lastUsed
и отображается в div#last-used
. Последние использованные эмодзи теперь видны!
Теперь, когда наш раздел lastUsed
отрисовывается правильно, мы можем убедиться, что он работает, выполнив следующую команду:
npm run build:push
Проверьте свою электронную таблицу смайликов, и вы должны увидеть, что последние использованные смайлики отображаются правильно. Благодаря этой функции пользователи теперь могут легко видеть и получать доступ к смайликам, которые они недавно использовали.
Тем не менее, есть небольшая проблема с нашей текущей настройкой. На данный момент наши смайлики lastUsed
не являются постоянными; они сбрасываются с каждым новым сеансом. Это означает, что когда пользователь закрывает и снова открывает приложение, его список последних использованных эмодзи исчезает.
Не волнуйтесь, мы рассмотрим эту проблему в следующем сообщении в блоге, где мы будем работать над тем, чтобы последние использованные смайлики сохранялись в разных сеансах. Оставайтесь с нами!
Содержание этой серии
- Часть 0: Худшие и лучшие практики
- Часть 1: объединение с Vite 🚀
- Часть 2. Объединение модуля NPM в скрипт Google Apps 📦
- Часть 3: CSS-преобразование Tailwind 🎨
- Часть 4: Приправьте свой проект зависимостями разработчиков! 🔧
- › Часть 5. Модульное тестирование внешнего интерфейса с помощью Jest 🚀
- Часть 6: Разговор между клиентом и сервером с помощью обещаний 🤝
- Часть 7: SPA и маршрутизация в интерфейсе GAS
- Часть 8. Развертывание внешнего интерфейса GAS в нескольких средах
- Часть 9: Работа с TypeScript в интерфейсе GAS
Этот пост был частично написан с помощью ChatGPT4
Обо мне
Я штатный разработчик Google Workspace и Google Cloud Platform, а также основатель Wurkspaces.dev. Если вы ищете надежного разработчика для своего проекта, наймите меня.
Купи мне кофе ☕ | Поддержите меня, став участником Medium
Дополнительные материалы на PlainEnglish.io.
Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .