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

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


Использование @open-wc/testing для Haunted

Для тестирования Haunted-компонентов чаще всего применяются инструменты из экосистемы Web Components, такие как @open-wc/testing. Она предоставляет удобные функции:

  • fixture(html``) — создание тестового экземпляра компонента в DOM.
  • expect() — assertion-библиотека с chai-подобным синтаксисом.
  • aTimeout(ms) — ожидание асинхронных обновлений DOM.

Пример базового теста компонента с хуком:

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

it('инициализирует состояние с useState', async () => {
  const el = await fixture(html`<my-component></my-component>`);
  
  // Ожидание обновления DOM после инициализации
  await aTimeout(0);

  const content = el.shadowRoot.textContent;
  expect(content).to.include('Состояние: 0');
});

Ключевой момент: хуки Haunted обновляют DOM асинхронно, поэтому важно использовать задержку (aTimeout) или updateComplete, чтобы гарантировать актуальное состояние перед проверкой.


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

useState возвращает текущее состояние и функцию для его изменения. Проверка работы useState обычно строится на:

  1. Инициализации состояния.
  2. Вызове функции обновления.
  3. Проверке рендеринга после изменения состояния.

Пример:

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

function Counter() {
  const [count, setCount] = useState(0);
  
  return html`
    <div>Счёт: ${count}</div>
    <button @click=${() => setCount(count + 1)}>Увеличить</button>
  `;
}

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

Тест:

it('увеличивает счет после клика', async () => {
  const el = await fixture(html`<x-counter></x-counter>`);
  const button = el.shadowRoot.querySelector('button');

  button.click();
  await aTimeout(0);

  expect(el.shadowRoot.textContent).to.include('Счёт: 1');
});

Тестирование useEffect и useLayoutEffect

useEffect в Haunted выполняется после рендера, useLayoutEffect — синхронно перед отображением. При тестировании это важно для контроля обновлений и побочных эффектов.

Пример теста для useEffect:

import { useEffect, useState } from 'haunted';

function FetchComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    setTimeout(() => setData('Данные загружены'), 50);
  }, []);

  return html`<div>${data || 'Загрузка...'}</div>`;
}

customElements.define('x-fetch', FetchComponent);

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

it('загружает данные после эффекта', async () => {
  const el = await fixture(html`<x-fetch></x-fetch>`);

  expect(el.shadowRoot.textContent).to.equal('Загрузка...');

  await aTimeout(60);
  expect(el.shadowRoot.textContent).to.equal('Данные загружены');
});

Моки и асинхронные операции

Для хуков, выполняющих HTTP-запросы или сложные побочные эффекты, рекомендуется использовать моки и spy-функции. Например, можно подменять fetch или использовать sinon для отслеживания вызовов.

import sinon from 'sinon';

it('вызывает fetch один раз', async () => {
  const fetchStub = sinon.stub(window, 'fetch').resolves({ json: () => ({}) });
  
  const el = await fixture(html`<x-fetch></x-fetch>`);
  await aTimeout(0);

  sinon.assert.calledOnce(fetchStub);
  fetchStub.restore();
});

Тестирование кастомных хуков

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

  1. Начальное состояние.
  2. Взаимодействие с другими хуками.
  3. Побочные эффекты при изменении аргументов.

Пример простого кастомного хука:

import { useState, useEffect } from 'haunted';

function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);

  const increment = () => setCount(c => c + 1);

  useEffect(() => {
    console.log('Счётчик изменён:', count);
  }, [count]);

  return [count, increment];
}

Тест:

it('кастомный хук изменяет значение', async () => {
  const el = await fixture(html`<x-counter></x-counter>`); 
  const [count, increment] = el.counterHook;

  expect(count).to.equal(0);

  increment();
  await aTimeout(0);

  expect(el.counterHook[0]).to.equal(1);
});

Советы по стабильности тестов

  • Использовать aTimeout или updateComplete для ожидания обновлений.
  • Разделять логику хуков и рендеринга для упрощения unit-тестов.
  • Подменять внешние зависимости через моки или spy.
  • Проверять не только DOM, но и внутреннее состояние хуков при необходимости.

Использование @testing-library с Haunted

Можно интегрировать @testing-library/dom для более «реалистичного» взаимодействия:

import { screen, fireEvent } from '@testing-library/dom';
import '@testing-library/jest-dom';
import { fixture, html } from '@open-wc/testing';

it('увеличивает счет при клике (Testing Library)', async () => {
  await fixture(html`<x-counter></x-counter>`);
  
  const button = screen.getByText('Увеличить');
  fireEvent.click(button);

  await aTimeout(0);
  expect(screen.getByText('Счёт: 1')).toBeInTheDocument();
});

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


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