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

Hyperapp — это компактный и функционально ориентированный JavaScript-фреймворк для создания пользовательских интерфейсов. В отличие от крупных библиотек, таких как React или Vue, Hyperapp сохраняет минимализм и простоту архитектуры, что накладывает особенности на процесс тестирования. Основной единицей работы здесь являются view-функции, которые возвращают описание DOM в виде виртуального дерева, и actions, управляющие состоянием приложения.

Тестирование в Hyperapp разделяется на несколько ключевых направлений:

  1. Тестирование чистых view-функций
  2. Тестирование действий (actions) и их побочных эффектов
  3. Интеграционное тестирование компонентов

Тестирование чистых view-функций

View-функции в Hyperapp принимают state и возвращают виртуальное DOM-дерево. Такие функции можно тестировать полностью изолированно, без необходимости запуска браузера.

Пример view-функции:

const Counter = (state) => (
  <div>
    <h1>{state.count}</h1>
    <button oncl ick={state.increment}>+</button>
    <button oncl ick={state.decrement}>-</button>
  </div>
);

Подход к тестированию:

  1. Вызывается view-функция с определённым состоянием.
  2. Проверяется структура возвращаемого дерева.
  3. Проверяются атрибуты элементов и их содержимое.

Пример теста с использованием Jest:

import { h } from "hyperapp";
import { Counter } from "./Counter";

test("Counter отображает правильное значение", () => {
  const state = { count: 5 };
  const view = Counter(state);
  
  expect(view.tag).toBe("div");
  expect(view.children[0].children[0]).toBe(5);
  expect(view.children[1].tag).toBe("button");
});

Ключевой момент — Hyperapp предоставляет виртуальное представление DOM, которое легко анализировать как обычный объект JavaScript. Для сложных компонентов можно рекурсивно проверять все дочерние узлы.


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

Actions в Hyperapp отвечают за изменение состояния и могут включать побочные эффекты (effects). Их тестирование разделяется на два подхода:

  1. Юнит-тестирование чистых actions Если action — чистая функция, тест проверяет только, как изменяется state.
const increment = (state) => ({ ...state, count: state.count + 1 });

test("increment увеличивает count", () => {
  const state = { count: 0 };
  const newState = increment(state);
  expect(newState.count).toBe(1);
});
  1. Тестирование с эффектами Для actions с асинхронными эффектами используется подмена эффектов через mock-функции.
const fetchData = (state) => [
  state,
  fetch("/data").then((res) => res.json())
];

test("fetchData вызывает fetch", async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({ json: () => Promise.resolve({ key: "value" }) })
  );

  const [state, effect] = fetchData({ items: [] });
  await effect;
  expect(global.fetch).toHaveBeenCalledWith("/data");
});

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


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

Для комплексного тестирования компонентов, когда важно проверить взаимодействие view и actions, применяются тестовые рендереры.

Использование jsdom для эмуляции DOM:

import { app, h } from "hyperapp";
import { Counter } from "./Counter";
import { JSDOM } from "jsdom";

const dom = new JSDOM(`<!DOCTYPE html><body><div id="root"></div></body>`);
global.document = dom.window.document;

const state = { count: 0, increment: (s) => ({ ...s, count: s.count + 1 }) };
app({ init: state, view: Counter, node: document.getElementById("root") });

// Проверка DOM
const buttons = document.querySelectorAll("button");
buttons[0].click();
expect(document.querySelector("h1").textContent).toBe("1");

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


Практические советы при тестировании Hyperapp

  • Изолировать view и actions: тестирование должно максимально отделять UI от бизнес-логики.
  • Использовать виртуальное DOM: проверка объектов без запуска браузера ускоряет тесты.
  • Мокать эффекты: асинхронные действия и взаимодействие с API лучше подменять заглушками.
  • Пошаговое тестирование: сначала юнит-тесты для функций, затем интеграционные тесты.
  • Рекурсивная проверка деревьев: сложные компоненты требуют глубокого анализа виртуального DOM.

Тщательное тестирование позволяет не только обнаружить ошибки, но и документировать ожидаемое поведение компонентов, что особенно важно в крупных Hyperapp-приложениях с множеством состояний и взаимодействий.