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

Haunted — это библиотека для создания веб-компонентов с реактивным рендерингом, основанная на Hooks в стиле React. При работе с Haunted тестирование интеграции играет ключевую роль, поскольку компоненты часто взаимодействуют друг с другом через состояние, события и DOM.


Настройка среды тестирования

Для тестирования интеграции в Haunted чаще всего используют комбинацию Jest и Testing Library (например, @testing-library/dom или @testing-library/web-component). Основные шаги:

  1. Установка зависимостей:
npm install --save-dev jest @testing-library/dom @testing-library/jest-dom
  1. Конфигурация Jest для работы с Web Components и Shadow DOM:
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
};
  1. Настройка функций рендеринга для Haunted-компонентов:
import { render } from 'haunted';
import { html } from 'lit-html';

export function renderComponent(Component, props = {}, container = document.body) {
  const el = document.createElement('div');
  container.appendChild(el);
  render(html`<${Component} ...=${props}></${Component}>`, el);
  return el;
}

Основные подходы к тестированию интеграции

Тестирование интеграции отличается от юнит-тестирования тем, что проверяется не изолированный компонент, а взаимодействие нескольких компонентов или компонентов с реальными DOM-элементами.

Проверка взаимодействия через события

Haunted-компоненты часто обмениваются данными через кастомные события.

import { html, component, useState } from 'haunted';

function Child({ onNotify }) {
  return html`<button @click=${() => onNotify('hello')}>Notify</button>`;
}

function Parent() {
  const [message, setMessage] = useState('');
  return html`
    <child-component @notify=${e => setMessage(e.detail)}></child-component>
    <span data-testid="msg">${message}</span>
  `;
}

customElements.define('child-component', component(Child));
customElements.define('parent-component', component(Parent));

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

import { screen, fireEvent } from '@testing-library/dom';
import { renderComponent } from './test-utils';
import './parent-component';

test('Parent получает событие от Child', () => {
  renderComponent('parent-component');
  const button = screen.getByText('Notify');
  fireEvent.click(button);
  const message = screen.getByTestId('msg');
  expect(message.textContent).toBe('hello');
});

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

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

function Counter() {
  const [count, setCount] = useState(0);
  return html`
    <span data-testid="count">${count}</span>
    <button @click=${() => setCount(count + 1)}>Increment</button>
  `;
}

customElements.define('counter-component', component(Counter));

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

test('Счётчик корректно увеличивает значение', () => {
  renderComponent('counter-component');
  const button = screen.getByText('Increment');
  const count = screen.getByTestId('count');

  expect(count.textContent).toBe('0');
  fireEvent.click(button);
  expect(count.textContent).toBe('1');
});

Работа с асинхронными действиями

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

function AsyncComponent() {
  const [data, setData] = useState('');
  useEffect(() => {
    setTimeout(() => setData('loaded'), 100);
  }, []);
  return html`<div data-testid="data">${data}</div>`;
}

customElements.define('async-component', component(AsyncComponent));

Тестирование с использованием waitFor:

import { waitFor } from '@testing-library/dom';

test('Асинхронное обновление данных', async () => {
  renderComponent('async-component');
  await waitFor(() => {
    expect(screen.getByTestId('data').textContent).toBe('loaded');
  });
});

Интеграция с внешними библиотеками

Haunted-компоненты могут взаимодействовать с библиотеками вроде Redux, RxJS или сторонними API. В интеграционном тестировании важно:

  1. Мокировать сторонние сервисы.
  2. Проверять корректное обновление состояния и DOM.
  3. Тестировать цепочку событий, включая пользовательские и системные.

Пример с RxJS:

import { BehaviorSubject } from 'rxjs';

const store$ = new BehaviorSubject(0);

function Display() {
  const [value, setValue] = useState(0);
  useEffect(() => {
    const sub = store$.subscribe(setValue);
    return () => sub.unsubscribe();
  }, []);
  return html`<span data-testid="value">${value}</span>`;
}

customElements.define('display-component', component(Display));

Тест:

test('Компонент корректно отображает данные из RxJS', () => {
  renderComponent('display-component');
  store$.next(42);
  expect(screen.getByTestId('value').textContent).toBe('42');
});

Тонкости тестирования Shadow DOM

Haunted использует Shadow DOM для изоляции стилей и структуры. Для интеграционных тестов важно:

  • Использовать методы shadowRoot.querySelector для доступа к внутренним элементам.
  • Применять @testing-library/dom с within для работы внутри Shadow DOM:
import { within } from '@testing-library/dom';

const host = document.querySelector('parent-component');
const shadow = within(host.shadowRoot);
const button = shadow.getByText('Notify');

Рекомендации по организации интеграционных тестов

  • Разделять тесты событий, тесты состояния и тесты асинхронного поведения.
  • Использовать утилиты для рендеринга компонентов, чтобы избежать дублирования кода.
  • Мокировать API и внешние зависимости для стабильно повторяемых тестов.
  • Всегда очищать DOM после тестов (afterEach(() => document.body.innerHTML = '')).

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