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

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

Основные принципы

  1. Тестирование взаимодействий, а не внутренней реализации Тесты должны проверять поведение компонентов снаружи: корректное отображение DOM, реакцию на события, изменения состояния. Внутренние методы и приватные свойства не являются объектом интеграционного тестирования.

  2. Изоляция внешних сервисов При взаимодействии с API или сторонними библиотеками используется мокирование или стабы. Это позволяет концентрироваться на взаимодействиях компонентов, а не на стабильности внешних систем.

  3. Воспроизводимость и предсказуемость Каждый тест должен создавать собственный чистый DOM-элемент и очищать его после выполнения, чтобы избежать побочных эффектов.

Настройка окружения для тестирования

Для интеграционных тестов компонентов Lit часто используют следующие инструменты:

  • @open-wc/testing — библиотека для тестирования веб-компонентов с поддержкой Mocha и Chai.
  • @web/test-runner — запускает тесты в реальном браузере.
  • Sinon.js — для мокирования функций и таймеров.

Пример базовой настройки:

import { fixture, html, expect } from '@open-wc/testing';
import './my-component.js';

describe('MyComponent integration', () => {
  it('взаимодействует с дочерним компонентом', async () => {
    const el = await fixture(html`<my-component></my-component>`);
    const child = el.shadowRoot.querySelector('child-component');
    expect(child).to.exist;
  });
});

Тестирование взаимодействия компонентов

В Lit компоненты обычно обмениваются данными через:

  • Свойства и атрибуты Свойства компонента можно изменять в тесте и проверять реакцию других компонентов:
el.someProp = 'value';
await el.updateComplete;
expect(el.shadowRoot.querySelector('span').textContent).to.equal('value');
  • События Компоненты могут генерировать события для информирования родителя или других компонентов:
const spy = sinon.spy();
el.addEventListener('custom-event', spy);
el.dispatchEvent(new CustomEvent('custom-event', { detail: 42 }));
expect(spy).to.have.been.calledOnceWith(sinon.match.has('detail', 42));
  • Слотинг и вложенные элементы Проверка того, что компоненты корректно взаимодействуют через слоты и контент:
const el = await fixture(html`
  <parent-component>
    <child-component slot="item"></child-component>
  </parent-component>
`);
const slotted = el.querySelector('child-component');
expect(slotted).to.exist;

Асинхронные обновления и updateComplete

Lit использует реактивную систему обновлений, поэтому важно учитывать асинхронность рендера. Любая проверка DOM должна выполняться после завершения обновления:

el.prop = 'new value';
await el.updateComplete;
expect(el.shadowRoot.querySelector('span').textContent).to.equal('new value');

Для тестирования цепочек событий и сложных реакций может понадобиться несколько последовательных await el.updateComplete.

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

Для интеграционных тестов часто нужно изолировать внешние сервисы:

import sinon from 'sinon';
import { fetchData } from './api.js';

const fetchStub = sinon.stub(window, 'fetch').resolves(new Response(JSON.stringify({ id: 1 })));

await el.loadData();
await el.updateComplete;

expect(el.data.id).to.equal(1);
fetchStub.restore();

Такой подход позволяет проверять корректность работы компонента при любых состояниях внешнего API без фактических сетевых запросов.

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

  • Создание чистой среды для каждого теста Использование fixture и удаление элементов после теста предотвращает утечки состояния.
  • Фокус на поведении, а не на DOM-структуре Проверка конкретных выходных эффектов и текстового содержимого более стабильна, чем проверка точного HTML.
  • Композиция тестов Меньшие интеграционные тесты, которые проверяют конкретное взаимодействие компонентов, легче поддерживать и отлаживать.

Пример комплексного интеграционного теста

it('проверяет взаимодействие родителя и дочернего компонента с событием', async () => {
  const el = await fixture(html`<parent-component></parent-component>`);
  const child = el.shadowRoot.querySelector('child-component');

  const result = [];
  el.addEventListener('child-action', (e) => result.push(e.detail));

  child.dispatchEvent(new CustomEvent('child-action', { detail: 'test' }));
  await el.updateComplete;

  expect(result).to.deep.equal(['test']);
  expect(el.shadowRoot.querySelector('span').textContent).to.equal('test');
});

Такой тест одновременно проверяет правильность генерации события дочерним компонентом, обработку события родителем и обновление DOM, что полностью соответствует цели интеграционного тестирования в Lit.

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