Модульное тестирование

Модульное тестирование в библиотеке Awesomplete применяется для проверки отдельных частей логики автодополнения без необходимости запускать полноценное браузерное окружение. Тестируются:

  • фильтрация элементов;
  • сортировка результатов;
  • обработка пользовательского ввода;
  • события выбора;
  • кастомные функции replace, filter, sort;
  • работа с DOM-элементами;
  • асинхронное обновление списка;
  • взаимодействие с клавиатурой.

Главная цель модульных тестов — гарантировать стабильность поведения компонента при изменении кода.


Архитектура тестирования Awesomplete

Большинство тестов для подобных UI-библиотек делится на несколько уровней:

Тип теста Проверяемая часть
Unit tests Отдельные методы и функции
Integration tests Взаимодействие компонентов
DOM tests Работа с HTML
Event tests Обработка событий
Async tests Асинхронная логика

Для Awesomplete наиболее важны именно unit- и DOM-тесты, поскольку библиотека активно взаимодействует с:

  • элементами <input>;
  • списками <ul>;
  • событиями клавиатуры;
  • пользовательским вводом.

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

Чаще всего используются:

Инструмент Назначение
Jest Тестовый раннер
Mocha Альтернативный раннер
Chai Assertions
Sinon Моки и шпионы
jsdom Эмуляция DOM
Vitest Современная альтернатива Jest

Установка окружения

Jest + jsdom

npm install --save-dev jest jsdom

Настройка package.json:

{
  "scripts": {
    "test": "jest"
  }
}

Настройка среды:

{
  "jest": {
    "testEnvironment": "jsdom"
  }
}

Базовый тест создания экземпляра

import Awesomplete from "awesomplete";

describe("Awesomplete initialization", () => {
    test("creates instance", () => {
        document.body.innerHTML =
            '<input id="city" />';

        const input = document.getElementById("city");

        const instance = new Awesomplete(input);

        expect(instance).toBeDefined();
    });
});

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

  • создание объекта;
  • корректная привязка к DOM;
  • отсутствие ошибок конструктора.

Проверка списка данных

Тест списка элементов

test("assigns list correctly", () => {
    document.body.innerHTML =
        '<input id="lang" />';

    const input = document.getElementById("lang");

    const awesomplete = new Awesomplete(input, {
        list: ["JavaScript", "Python", "Rust"]
    });

    expect(awesomplete._list.length).toBe(3);
});

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

Awesomplete использует функцию FILTER_CONTAINS и другие механизмы фильтрации.

Проверка встроенного фильтра

test("filters matching values", () => {
    document.body.innerHTML =
        '<input id="test" />';

    const input = document.getElementById("test");

    const awesomplete = new Awesomplete(input, {
        list: ["JavaScript", "Java", "Python"]
    });

    input.value = "Java";

    const result = awesomplete._list.filter(item =>
        Awesomplete.FILTER_CONTAINS(item, input.value)
    );

    expect(result.length).toBe(2);
});

Проверка пользовательского фильтра

test("uses custom filter", () => {
    document.body.innerHTML =
        '<input id="custom" />';

    const input = document.getElementById("custom");

    const awesomplete = new Awesomplete(input, {
        list: ["Cat", "Dog", "Bird"],
        filter: (text, input) => {
            return text.startsWith(input);
        }
    });

    input.value = "C";

    const result = awesomplete._list.filter(item =>
        awesomplete.filter(item, input.value)
    );

    expect(result[0]).toBe("Cat");
});

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

Проверка сортировки результатов

test("sorts items alphabetically", () => {
    const items = ["Python", "Java", "C++"];

    items.sort(Awesomplete.SORT_BYLENGTH);

    expect(items[0]).toBe("Java");
});

Тестирование отображения списка

Awesomplete динамически создаёт элементы <li>.

Проверка рендера

test("renders suggestion list", () => {
    document.body.innerHTML =
        '<input id="render" />';

    const input = document.getElementById("render");

    const awesomplete = new Awesomplete(input, {
        list: ["Apple", "Orange"]
    });

    input.value = "A";

    awesomplete.evaluate();

    const items =
        awesomplete.ul.children;

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

Проверка содержимого DOM

test("renders correct text", () => {
    document.body.innerHTML =
        '<input id="fruit" />';

    const input = document.getElementById("fruit");

    const awesomplete = new Awesomplete(input, {
        list: ["Apple"]
    });

    input.value = "A";

    awesomplete.evaluate();

    const text =
        awesomplete.ul.children[0].textContent;

    expect(text).toContain("Apple");
});

Проверка событий

Awesomplete генерирует несколько важных событий:

Событие Назначение
awesomplete-open Открытие списка
awesomplete-close Закрытие
awesomplete-select Выбор элемента
awesomplete-highlight Наведение

Тестирование события выбора

test("fires sel ect event", () => {
    document.body.innerHTML =
        '<input id="event" />';

    const input = document.getElementById("event");

    const awesomplete = new Awesomplete(input, {
        list: ["JavaScript"]
    });

    const handler = jest.fn();

    input.addEventListener(
        "awesomplete-select",
        handler
    );

    input.value = "J";

    awesomplete.evaluate();

    awesomplete.select(
        awesomplete.ul.children[0]
    );

    expect(handler).toHaveBeenCalled();
});

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

test("opens suggestion panel", () => {
    document.body.innerHTML =
        '<input id="open" />';

    const input = document.getElementById("open");

    const awesomplete = new Awesomplete(input, {
        list: ["One"]
    });

    input.value = "O";

    awesomplete.evaluate();

    expect(
        awesomplete.ul.hasAttribute("hidden")
    ).toBe(false);
});

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

test("closes suggestion panel", () => {
    document.body.innerHTML =
        '<input id="close" />';

    const input = document.getElementById("close");

    const awesomplete = new Awesomplete(input, {
        list: ["One"]
    });

    awesomplete.close();

    expect(
        awesomplete.ul.hasAttribute("hidden")
    ).toBe(true);
});

Тестирование клавиатурной навигации

Awesomplete активно использует:

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

Проверка перемещения выделения

test("moves selection with keyboard", () => {
    document.body.innerHTML =
        '<input id="keyboard" />';

    const input = document.getElementById("keyboard");

    const awesomplete = new Awesomplete(input, {
        list: ["One", "Two"]
    });

    input.value = "O";

    awesomplete.evaluate();

    awesomplete.next();

    expect(
        awesomplete.index
    ).toBe(0);
});

Проверка выбора Enter

test("selects item with enter", () => {
    document.body.innerHTML =
        '<input id="enter" />';

    const input = document.getElementById("enter");

    const awesomplete = new Awesomplete(input, {
        list: ["JavaScript"]
    });

    input.value = "J";

    awesomplete.evaluate();

    awesomplete.next();

    awesomplete.select();

    expect(input.value).toBe("JavaScript");
});

Тестирование пользовательской функции replace

Проверка замены значения

test("uses custom replace", () => {
    document.body.innerHTML =
        '<input id="replace" />';

    const input = document.getElementById("replace");

    const awesomplete = new Awesomplete(input, {
        list: ["JavaScript"],
        replace: function(text) {
            this.input.value =
                text.toUpperCase();
        }
    });

    awesomplete.replace("JavaScript");

    expect(input.value)
        .toBe("JAVASCRIPT");
});

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

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

test("updates async list", async () => {
    document.body.innerHTML =
        '<input id="async" />';

    const input = document.getElementById("async");

    const awesomplete =
        new Awesomplete(input);

    const data =
        await Promise.resolve([
            "Alpha",
            "Beta"
        ]);

    awesomplete.list = data;

    expect(
        awesomplete._list.length
    ).toBe(2);
});

Использование mock-функций

Проверка вызовов обработчиков

test("calls callback", () => {
    const callback = jest.fn();

    callback();

    expect(callback)
        .toHaveBeenCalledTimes(1);
});

Использование spy-объектов

test("spies on evaluate", () => {
    document.body.innerHTML =
        '<input id="spy" />';

    const input =
        document.getElementById("spy");

    const awesomplete =
        new Awesomplete(input);

    const spy =
        jest.spyOn(awesomplete, "evaluate");

    awesomplete.evaluate();

    expect(spy)
        .toHaveBeenCalled();
});

Изоляция DOM

Каждый тест должен работать независимо.

Очистка DOM

afterEach(() => {
    document.body.innerHTML = "";
});

Без очистки:

  • появляются ложные результаты;
  • сохраняются старые элементы;
  • тесты влияют друг на друга.

Тестирование edge-case сценариев

Особое внимание уделяется нестандартным ситуациям.

Пустой список

test("handles empty list", () => {
    document.body.innerHTML =
        '<input id="empty" />';

    const input = document.getElementById("empty");

    const awesomplete =
        new Awesomplete(input, {
            list: []
        });

    input.value = "A";

    awesomplete.evaluate();

    expect(
        awesomplete.ul.children.length
    ).toBe(0);
});

Некорректные значения

test("handles null values", () => {
    document.body.innerHTML =
        '<input id="null" />';

    const input =
        document.getElementById("null");

    expect(() => {
        new Awesomplete(input, {
            list: null
        });
    }).not.toThrow();
});

Проверка больших наборов данных

test("works with large dataset", () => {
    document.body.innerHTML =
        '<input id="large" />';

    const input =
        document.getElementById("large");

    const data =
        Array.fr om(
            { length: 10000 },
            (_, i) => `Item ${i}`
        );

    const awesomplete =
        new Awesomplete(input, {
            list: data
        });

    expect(
        awesomplete._list.length
    ).toBe(10000);
});

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

Для UI-компонентов важно контролировать:

  • скорость фильтрации;
  • время рендера;
  • количество операций DOM;
  • число повторных вычислений.

Пример простого performance-теста

test("filters quickly", () => {
    const data =
        Array.from(
            { length: 5000 },
            (_, i) => `Element ${i}`
        );

    const start = performance.now();

    data.filter(item =>
        item.includes("100")
    );

    const end = performance.now();

    expect(end - start)
        .toBeLessThan(10);
});

Проверка утечек памяти

В UI-библиотеках возможны проблемы:

  • не удаляются обработчики;
  • сохраняются DOM-ссылки;
  • растут внутренние массивы.

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

test("removes references", () => {
    document.body.innerHTML =
        '<input id="memory" />';

    const input =
        document.getElementById("memory");

    let awesomplete =
        new Awesomplete(input);

    awesomplete = null;

    expect(awesomplete)
        .toBeNull();
});

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

Jest позволяет сохранять DOM-состояния.

Пример snapshot-теста

test("matches snapshot", () => {
    document.body.innerHTML =
        '<input id="snap" />';

    const input =
        document.getElementById("snap");

    const awesomplete =
        new Awesomplete(input, {
            list: ["One"]
        });

    input.value = "O";

    awesomplete.evaluate();

    expect(
        awesomplete.ul.innerHTML
    ).toMatchSnapshot();
});

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

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

tests/
├── initialization.test.js
├── filtering.test.js
├── rendering.test.js
├── events.test.js
├── keyboard.test.js
├── async.test.js
└── performance.test.js

Практика именования тестов

Хорошее имя теста:

test("opens dropdown after typing")

Плохое имя:

test("works")

Название должно описывать:

  • действие;
  • условие;
  • ожидаемый результат.

Принцип AAA

Структура большинства тестов:

Этап Описание
Arrange Подготовка
Act Выполнение
Assert Проверка

Пример

test("selects suggestion", () => {

    // Arrange
    document.body.innerHTML =
        '<input id="aaa" />';

    const input =
        document.getElementById("aaa");

    const awesomplete =
        new Awesomplete(input, {
            list: ["Alpha"]
        });

    // Act
    input.value = "A";

    awesomplete.evaluate();

    awesomplete.select();

    // Assert
    expect(input.value)
        .toBe("Alpha");
});

Частые ошибки при тестировании Awesomplete

Отсутствие jsdom

Без DOM-среды тесты падают:

document is not defined

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

Тестирование _list и других приватных полей делает тесты хрупкими.

Лучше проверять:

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

Слишком большие unit-тесты

Один тест должен проверять одну вещь.

Плохо:

test("everything works")

Хорошо:

test("opens dropdown")
test("filters items")
test("selects value")

Использование CI

Модульные тесты обычно запускаются:

  • GitHub Actions;
  • GitLab CI;
  • Jenkins;
  • TeamCity.

Пример 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 поддерживает coverage-анализ.

Запуск coverage

npm test -- --coverage

Метрики покрытия

Метрика Описание
Statements Выполненные инструкции
Branches Ветвления
Functions Функции
Lines Строки

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

Наиболее эффективный подход включает:

  1. Проверку пользовательского поведения.
  2. Минимизацию зависимости от внутренних свойств.
  3. Изоляцию тестов.
  4. Проверку событий.
  5. Тестирование DOM.
  6. Проверку нестандартных сценариев.
  7. Контроль производительности.
  8. Проверку асинхронной логики.
  9. Snapshot-тестирование.
  10. Автоматический запуск в CI.