Уверенность в каждой строке кода: использование 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.

Для этой интеграции нам нужно выполнить несколько задач:

  1. Добавление смайликов в наш класс LastUsed при нажатии на них
  2. Визуализация смайликов в нижнем колонтитуле 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 не являются постоянными; они сбрасываются с каждым новым сеансом. Это означает, что когда пользователь закрывает и снова открывает приложение, его список последних использованных эмодзи исчезает.

Не волнуйтесь, мы рассмотрим эту проблему в следующем сообщении в блоге, где мы будем работать над тем, чтобы последние использованные смайлики сохранялись в разных сеансах. Оставайтесь с нами!

Содержание этой серии

  1. Часть 0: Худшие и лучшие практики
  2. Часть 1: объединение с Vite 🚀
  3. Часть 2. Объединение модуля NPM в скрипт Google Apps 📦
  4. Часть 3: CSS-преобразование Tailwind 🎨
  5. Часть 4: Приправьте свой проект зависимостями разработчиков! 🔧
  6. › Часть 5. Модульное тестирование внешнего интерфейса с помощью Jest 🚀
  7. Часть 6: Разговор между клиентом и сервером с помощью обещаний 🤝
  8. Часть 7: SPA и маршрутизация в интерфейсе GAS
  9. Часть 8. Развертывание внешнего интерфейса GAS в нескольких средах
  10. Часть 9: Работа с TypeScript в интерфейсе GAS

Этот пост был частично написан с помощью ChatGPT4

Обо мне

Я штатный разработчик Google Workspace и Google Cloud Platform, а также основатель Wurkspaces.dev. Если вы ищете надежного разработчика для своего проекта, наймите меня.

Купи мне кофе ☕ | Поддержите меня, став участником Medium

Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .