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

Интеграционное тестирование предназначено для проверки корректной работы библиотеки Awesomplete в составе более крупного приложения. Если модульное тестирование оценивает отдельные методы и функции изолированно, то интеграционные тесты исследуют взаимодействие между компонентами системы: пользовательским интерфейсом, сетевыми запросами, обработчиками событий, внешними библиотеками и серверной частью.

Для автодополнения особенно важны сценарии, в которых библиотека получает данные из внешних источников, взаимодействует с формами и реагирует на действия пользователя.

Основные задачи интеграционного тестирования:

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

Отличия интеграционных тестов от модульных

Модульный тест может проверять работу фильтра списка:

expect(filter("Ja")).toEqual([
    "Java",
    "JavaScript"
]);

Такой тест не показывает, будет ли список действительно отображён в интерфейсе.

Интеграционный тест охватывает весь процесс:

  1. Пользователь вводит текст.
  2. Выполняется поиск совпадений.
  3. Awesomplete получает результаты.
  4. Формируется список подсказок.
  5. Список отображается в DOM.
  6. Пользователь выбирает вариант.
  7. Поле ввода получает новое значение.

Проверяется полный сценарий использования.


Уровни интеграции

Интеграция с DOM

Самый распространённый вариант тестирования.

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

  • создание списка подсказок;
  • добавление CSS-классов;
  • изменение структуры документа;
  • реакция на пользовательский ввод.

Пример подготовки среды:

document.body.innerHTML = `
    <input id="search">
`;

const input = document.querySelector("#search");

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

После ввода значения проверяется состояние DOM:

input.value = "Ja";
input.dispatchEvent(new Event("input"));

expect(
    document.querySelectorAll("li").length
).toBe(2);

Интеграция с сетевым слоем

Часто данные для автодополнения загружаются через API.

Схема работы:

Input
  ↓
fetch()
  ↓
JSON
  ↓
Awesomplete.list
  ↓
Dropdown

Тест проверяет всю цепочку.

Пример:

global.fetch = jest.fn(() =>
    Promise.resolve({
        json: () => Promise.resolve([
            "Java",
            "JavaScript"
        ])
    })
);

Код приложения:

input.addEventListener("input", async () => {
    const response = await fetch("/search");

    awesomplete.list =
        await response.json();
});

Интеграционный тест:

input.value = "Ja";

input.dispatchEvent(
    new Event("input")
);

await Promise.resolve();

expect(fetch)
    .toHaveBeenCalled();

Проверяется взаимодействие между пользовательским вводом, сетью и компонентом автодополнения.


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

Интеграционные тесты ориентированы на реальные действия пользователя.

Сценарий выбора элемента

Подготовка:

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

Имитация ввода:

input.value = "Ja";

input.dispatchEvent(
    new Event("input")
);

Выбор первого варианта:

const firstItem =
    document.querySelector("li");

firstItem.click();

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

expect(input.value)
    .toBe("Java");

Тест отражает типичный пользовательский процесс.


Сценарий очистки поля

input.value = "";
input.dispatchEvent(
    new Event("input")
);

Проверка отсутствия подсказок:

expect(
    document.querySelector("ul")
        .children.length
).toBe(0);

Сценарий повторного поиска

input.value = "J";
input.dispatchEvent(new Event("input"));

input.value = "Ja";
input.dispatchEvent(new Event("input"));

input.value = "Jav";
input.dispatchEvent(new Event("input"));

Проверяется корректное обновление списка на каждом этапе.


Интеграция с серверным API

Во многих проектах список вариантов хранится на сервере.

Пример приложения:

async function loadSuggestions(query) {
    const response =
        await fetch(`/api?q=${query}`);

    return response.json();
}

Интеграционный тест должен удостовериться, что:

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

Проверка:

expect(fetch)
    .toHaveBeenCalledWith(
        "/api?q=java"
    );

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

Awesomplete часто используется совместно с асинхронными запросами.

Пример:

input.addEventListener(
    "input",
    async () => {

        const result =
            await fetchData();

        awesomplete.list = result;
    }
);

Тест:

await waitFor(() => {
    expect(
        awesomplete.list.length
    ).toBeGreaterThan(0);
});

Проверяется завершение всей асинхронной цепочки.


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

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

Создание поля:

render(`
    <input id="search">
`);

Поиск элемента:

const input =
    screen.getByRole("textbox");

Ввод текста:

await userEvent.type(
    input,
    "Ja"
);

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

expect(
    screen.getByText("Java")
).toBeInTheDocument();

Такой подход уменьшает зависимость тестов от внутренней реализации компонента.


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

Awesomplete генерирует собственные события.

Наиболее важные:

  • awesomplete-open;
  • awesomplete-close;
  • awesomplete-highlight;
  • awesomplete-select;
  • awesomplete-selectcomplete.

Интеграционный тест позволяет убедиться, что события возникают в нужной последовательности.

Пример:

const handler = jest.fn();

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

Выбор элемента:

document
    .querySelector("li")
    .click();

Проверка:

expect(handler)
    .toHaveBeenCalled();

Проверка взаимодействия с формами

Автодополнение редко используется отдельно от форм.

Типичный сценарий:

<form id="user-form">
    <input id="city">
</form>

После выбора подсказки:

expect(city.value)
    .toBe("London");

Дополнительно проверяется отправка формы:

form.dispatchEvent(
    new Event("submit")
);

Если поле участвует в бизнес-логике приложения, интеграционный тест должен удостовериться в правильности всей цепочки обработки.


Интеграция с React

В React Awesomplete обычно создаётся внутри жизненного цикла компонента.

Пример:

useEffect(() => {
    new Awesomplete(
        inputRef.current,
        {
            list
        }
    );
}, [list]);

Интеграционный тест проверяет:

  • создание экземпляра;
  • обновление данных;
  • уничтожение компонента;
  • отсутствие утечек памяти.

Пример:

render(<Search />);

expect(
    document.querySelector("input")
).toBeTruthy();

После обновления состояния:

rerender(
    <Search data={newData}/>
);

Проверяется появление новых вариантов.


Интеграция с Vue

Пример инициализации:

mounted() {
    this.awesomplete =
        new Awesomplete(
            this.$refs.input
        );
}

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

const wrapper =
    mount(Component);

Поиск поля:

const input =
    wrapper.find("input");

Имитация ввода:

await input.setValue("Ja");

Проверка отображения подсказок:

expect(
    document.querySelectorAll("li")
        .length
).toBeGreaterThan(0);

Интеграция с jQuery

Во многих старых проектах Awesomplete используется совместно с jQuery.

Пример:

$("#search").on(
    "change",
    updateData
);

Интеграционный тест проверяет, что:

  • событие возникает корректно;
  • значение поля обновляется;
  • обработчики выполняются после выбора подсказки.

Проверка:

const spy = jest.fn();

$("#search").on(
    "change",
    spy
);

После выбора элемента:

expect(spy)
    .toHaveBeenCalled();

Проверка обработки ошибок

Интеграционные тесты должны охватывать аварийные сценарии.

Например, сервер недоступен:

global.fetch = jest.fn(() =>
    Promise.reject(
        new Error("Network Error")
    )
);

Проверка:

await expect(
    loadSuggestions("java")
)
.rejects.toThrow();

Другой вариант — получение пустого списка:

Promise.resolve([]);

Ожидаемое поведение:

expect(
    document.querySelectorAll("li")
        .length
).toBe(0);

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

При больших объёмах данных важно удостовериться, что интеграция не приводит к деградации интерфейса.

Создание большого массива:

const items =
    Array.from(
        { length: 10000 },
        (_, i) => `Item ${i}`
    );

Инициализация:

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

Измерение времени:

const start =
    performance.now();

input.value = "Item";

input.dispatchEvent(
    new Event("input")
);

const end =
    performance.now();

Проверка:

expect(end - start)
    .toBeLessThan(100);

Подобные тесты позволяют обнаруживать проблемы взаимодействия между алгоритмами поиска и интерфейсом.


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

Крупные проекты обычно группируют тесты по пользовательским сценариям.

Пример структуры:

tests/
│
├── integration/
│   ├── search.test.js
│   ├── api.test.js
│   ├── form.test.js
│   ├── events.test.js
│   └── errors.test.js

Внутри сценариев выделяются отдельные группы проверок:

describe(
    "Search Integration",
    () => {

        test(
            "loads suggestions",
            () => {}
        );

        test(
            "selects item",
            () => {}
        );

        test(
            "updates form",
            () => {}
        );
    }
);

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