Юнит-тестирование компонентов

Юнит-тестирование компонентов, построенных вокруг библиотеки Tom Select, позволяет контролировать корректность поведения пользовательского интерфейса при изменении конфигурации, подключении плагинов, обновлении зависимостей и масштабировании приложения. Основная задача тестов — гарантировать, что компонент выбора элементов работает одинаково стабильно вне зависимости от внешних условий.

Компоненты Tom Sel ect часто содержат:

  • сложную обработку DOM;
  • асинхронную загрузку данных;
  • фильтрацию;
  • пользовательские события;
  • динамическое создание option-элементов;
  • интеграцию с React, Vue или другими фреймворками;
  • плагины и пользовательские расширения.

Без тестирования такие компоненты быстро становятся источником регрессий.


Что именно тестируется

Юнит-тестирование компонентов Tom Select обычно охватывает следующие области:

Инициализация экземпляра

Проверяется:

  • создаётся ли экземпляр;
  • подключаются ли настройки;
  • применяется ли конфигурация;
  • существуют ли необходимые DOM-узлы.

Пример:

const select = document.createElement('select');

select.innerHTML = `
  <option value="js">JavaScript</option>
`;

document.body.appendChild(select);

const tom = new TomSelect(select);

expect(tom).toBeDefined();
expect(tom.options.js.text).toBe('JavaScript');

Работа со значениями

Проверяется:

  • установка значения;
  • получение значения;
  • множественный выбор;
  • очистка;
  • удаление элементов.
tom.setValue('js');

expect(tom.getValue()).toBe('js');

tom.clear();

expect(tom.getValue()).toBe('');

Работа поиска

Проверяется:

  • фильтрация;
  • поиск по нескольким полям;
  • отсутствие совпадений;
  • пользовательские функции поиска.
const results = tom.search('java');

expect(results.items.length).toBeGreaterThan(0);

События

Проверяется:

  • вызов callback-функций;
  • порядок событий;
  • корректность аргументов.
const onCha nge = jest.fn();

const tom = new TomSelect(select, {
  onChange
});

tom.setValue('js');

expect(onChange).toHaveBeenCalled();
expect(onChange).toHaveBeenCalledWith('js');

Асинхронная загрузка

Проверяется:

  • корректность load-функции;
  • обработка ошибок;
  • обновление options;
  • debounce;
  • отмена предыдущих запросов.

Инструменты тестирования

Jest

Наиболее популярный инструмент для unit-тестов JavaScript-компонентов.

Основные возможности:

  • assertions;
  • mocks;
  • spies;
  • fake timers;
  • snapshot testing.

Установка:

npm install --save-dev jest

Vitest

Современная альтернатива Jest, особенно популярная в проектах с Vite.

Установка:

npm install --save-dev vitest

Конфигурация:

// vite.config.js
export default defineConfig({
  test: {
    environment: 'jsdom'
  }
});

JSDOM

Tom Select активно взаимодействует с DOM, поэтому тестовая среда должна эмулировать браузер.

JSDOM предоставляет:

  • document;
  • window;
  • HTMLElement;
  • события;
  • querySelector;
  • form API.

Без JSDOM значительная часть тестов работать не будет.


Testing Library

При тестировании UI-компонентов удобно использовать:

npm install --save-dev @testing-library/dom

или:

npm install --save-dev @testing-library/react

Testing Library позволяет тестировать компонент ближе к пользовательскому поведению.


Базовая структура теста

Типичный unit-тест Tom Select состоит из нескольких этапов:

  1. Создание DOM.
  2. Инициализация компонента.
  3. Выполнение действия.
  4. Проверка результата.
  5. Очистка.

Пример:

describe('TomSelect', () => {

  let select;
  let tom;

  beforeEach(() => {

    document.body.innerHTML = `
      <select id="skills">
        <option value="js">JavaScript</option>
        <option value="ts">TypeScript</option>
      </select>
    `;

    select = document.querySelector('#skills');

    tom = new TomSelect(select);

  });

  afterEach(() => {
    tom.destroy();
    document.body.innerHTML = '';
  });

  test('setValue устанавливает значение', () => {

    tom.setValue('ts');

    expect(tom.getValue()).toBe('ts');

  });

});

Тестирование DOM-структуры

Tom Select создаёт большое количество дополнительных элементов:

  • wrapper;
  • dropdown;
  • control;
  • items;
  • input;
  • option.

Проверка DOM позволяет убедиться, что интерфейс строится корректно.

expect(
  document.querySelector('.ts-wrapper')
).not.toBeNull();

expect(
  document.querySelector('.ts-dropdown')
).not.toBeNull();

Проверка CSS-классов

Tom Select динамически изменяет классы состояния.

Проверяются:

  • focus;
  • disabled;
  • dropdown-active;
  • has-items;
  • full;
  • input-active.

Пример:

tom.focus();

expect(
  tom.wrapper.classList.contains('focus')
).toBe(true);

Тестирование множественного выбора

document.body.innerHTML = `
  <select multiple id="tags">
    <option value="js">JS</option>
    <option value="vue">Vue</option>
    <option value="react">React</option>
  </select>
`;

const tom = new TomSelect('#tags');

tom.setValue(['js', 'vue']);

expect(tom.items.length).toBe(2);
expect(tom.getValue()).toEqual(['js', 'vue']);

Проверка удаления элементов

tom.removeItem('js');

expect(tom.items.includes('js')).toBe(false);

Тестирование пользовательского ввода

Tom Select активно работает с текстовым input.

tom.control_input.value = 'react';

tom.onInput();

expect(tom.lastValue).toBe('react');

Проверка открытия dropdown

tom.open();

expect(tom.isOpen).toBe(true);

Проверка закрытия:

tom.close();

expect(tom.isOpen).toBe(false);

Тестирование create-функции

Если включён режим создания новых элементов:

const tom = new TomSelect(select, {
  create: true
});

tom.createItem('Node.js');

expect(tom.options['Node.js']).toBeDefined();

Тестирование render-функций

Tom Select позволяет полностью переопределять шаблоны.

Пример:

const tom = new TomSelect(select, {
  render: {
    option(data, escape) {
      return `
        <div class="custom-option">
          ${escape(data.text)}
        </div>
      `;
    }
  }
});

Тест:

tom.refreshOptions();

expect(
  document.querySelector('.custom-option')
).not.toBeNull();

Snapshot-тестирование

Snapshot позволяет отслеживать изменения DOM.

Пример для Jest:

expect(tom.wrapper.outerHTML).toMatchSnapshot();

Snapshot особенно полезен:

  • при сложных шаблонах;
  • кастомном render;
  • большом количестве плагинов.

Тестирование асинхронной загрузки

Проверка load callback

const loadMock = jest.fn((query, callback) => {

  callback([
    { value: '1', text: 'JavaScript' }
  ]);

});

const tom = new TomSelect(select, {
  load: loadMock
});

tom.load('java');

expect(loadMock).toHaveBeenCalled();

Проверка обновления options

expect(tom.options['1']).toBeDefined();

Мокирование fetch

Асинхронная загрузка обычно использует fetch.

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve([
      { id: 1, name: 'JavaScript' }
    ])
  })
);

Тестирование debounce

Tom Select может использовать задержку запросов.

jest.useFakeTimers();

const load = jest.fn();

const tom = new TomSelect(select, {
  loadThrottle: 300,
  load
});

tom.onInput();

jest.advanceTimersByTime(300);

expect(load).toHaveBeenCalled();

Проверка destroy

Destroy особенно важен для SPA-приложений.

Проверяется:

  • удаление обработчиков;
  • очистка DOM;
  • предотвращение memory leaks.
tom.destroy();

expect(
  document.querySelector('.ts-wrapper')
).toBeNull();

Тестирование disabled-состояния

tom.disable();

expect(tom.isDisabled).toBe(true);

Повторное включение:

tom.enable();

expect(tom.isDisabled).toBe(false);

Тестирование placeholder

const tom = new TomSelect(select, {
  placeholder: 'Выберите язык'
});

expect(
  tom.control_input.placeholder
).toBe('Выберите язык');

Проверка optgroup

document.body.innerHTML = `
<select id="skills">
  <optgroup label="Frontend">
    <option value="js">JS</option>
  </optgroup>
</select>
`;

const tom = new TomSelect('#skills');

expect(
  tom.optgroups
).toBeDefined();

Тестирование пользовательских плагинов

Плагины должны тестироваться отдельно от основного компонента.

Пример:

TomSelect.define('test_plugin', function() {

  this.testMethod = () => 'plugin';

});

Тест:

const tom = new TomSelect(select, {
  plugins: ['test_plugin']
});

expect(tom.testMethod()).toBe('plugin');

Изоляция тестов

Одна из ключевых задач — исключение влияния тестов друг на друга.

Для этого:

  • очищается DOM;
  • уничтожается экземпляр;
  • сбрасываются mocks;
  • очищаются timers.

Пример:

afterEach(() => {

  jest.clearAllMocks();

  document.body.innerHTML = '';

});

Тестирование React-компонентов

Tom Select часто оборачивается в React-компонент.

Пример:

function SelectComponent() {

  useEffect(() => {

    const tom = new TomSelect('#skills');

    return () => tom.destroy();

  }, []);

  return (
    <select id="skills">
      <option value="js">JS</option>
    </select>
  );
}

Тест:

import { render } fr om '@testing-library/react';

test('рендер компонента', () => {

  render(<SelectComponent />);

  expect(
    document.querySelector('.ts-wrapper')
  ).not.toBeNull();

});

Тестирование пользовательских событий

Tom Select генерирует события:

  • item_add;
  • item_remove;
  • change;
  • dropdown_open;
  • dropdown_close;
  • type.

Пример:

const handler = jest.fn();

tom.on('item_add', handler);

tom.addItem('js');

expect(handler).toHaveBeenCalled();

Проверка keyboard navigation

Tom Select активно использует клавиатурное управление.

Тестируются:

  • Enter;
  • Backspace;
  • Escape;
  • ArrowUp;
  • ArrowDown;
  • Tab.

Пример:

const event = new KeyboardEvent('keydown', {
  key: 'ArrowDown'
});

tom.control.dispatchEvent(event);

Тестирование фильтрации

const result = tom.search('react');

expect(result.items.length).toBe(1);

Проверка пользовательских searchField

const tom = new TomSelect(select, {
  searchField: ['title', 'description']
});

Тест:

expect(tom.settings.searchField)
  .toContain('description');

Тестирование preload

const load = jest.fn();

const tom = new TomSelect(select, {
  preload: true,
  load
});

expect(load).toHaveBeenCalled();

Проверка maxItems

const tom = new TomSelect(select, {
  maxItems: 2
});

tom.addItem('js');
tom.addItem('vue');
tom.addItem('react');

expect(tom.items.length).toBe(2);

Проверка persist

const tom = new TomSelect(select, {
  create: true,
  persist: false
});

tom.createItem('Custom');

tom.clear();

expect(tom.options.Custom).toBeUndefined();

Стратегии тестирования

Тестирование поведения вместо реализации

Предпочтительно проверять:

  • поведение интерфейса;
  • результат действий;
  • изменение DOM.

Менее надёжны тесты внутренних приватных методов.


Минимизация snapshot

Избыточные snapshot-тесты усложняют поддержку.

Лучше:

  • проверять ключевые элементы;
  • использовать точечные assertions;
  • тестировать пользовательские сценарии.

Независимость тестов

Каждый тест должен запускаться отдельно без зависимости от предыдущих.


Минимизация моков

Чрезмерное мокирование делает тесты искусственными.

Следует мокировать:

  • HTTP;
  • таймеры;
  • внешние сервисы.

Не следует мокировать сам DOM без необходимости.


Типичные проблемы

Отсутствие destroy

Без destroy:

  • остаются listeners;
  • загрязняется DOM;
  • ломаются следующие тесты.

Ошибки async-тестов

Неправильная работа с Promise вызывает:

  • ложноположительные результаты;
  • race condition;
  • нестабильные тесты.

Проверка внутренних свойств

Например:

expect(tom.sifter).toBeDefined();

Такие тесты плохо переживают обновления библиотеки.


Интеграционное тестирование

Хотя unit-тесты важны, многие сценарии Tom Select лучше проверяются интеграционно:

  • взаимодействие с API;
  • работа форм;
  • интеграция с фреймворками;
  • пользовательские сценарии.

Для этого часто используют:

  • Cypress;
  • Playwright;
  • Testing Library.

Организация тестов

Типичная структура:

tests/
├── unit/
│   ├── tom-select.spec.js
│   ├── plugins.spec.js
│   ├── async.spec.js
│   └── render.spec.js
│
├── integration/
│   ├── forms.spec.js
│   └── api.spec.js

Покрытие тестами

Наиболее критичные зоны:

Область Приоритет
Выбор значений Высокий
События Высокий
Async load Высокий
destroy Высокий
Keyboard navigation Средний
Render templates Средний
CSS-классы Низкий

Автоматизация тестирования

Тесты обычно запускаются:

  • локально;
  • в pre-commit hooks;
  • в CI/CD;
  • при pull request.

Пример GitHub Actions:

name: tests

on: [push]

jobs:
  test:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: actions/setup-node@v3
        with:
          node-version: 20

      - run: npm install

      - run: npm test

Проверка покрытия кода

Jest:

npm test -- --coverage

Vitest:

vitest --coverage

Покрытие помогает обнаруживать:

  • непроверенные ветки;
  • отсутствующие сценарии;
  • неиспользуемый код.

Однако высокий coverage не гарантирует качество тестов, если assertions не проверяют реальное поведение компонента.