Unit-тестирование компонентов

Unit-тестирование в Mithril.js позволяет изолированно проверять поведение компонентов, их рендеринг, обработку событий и взаимодействие с состоянием. Основной целью является подтверждение корректности работы логики и структуры компонентов без необходимости поднимать весь фронтенд-приложение.


Основы тестирования компонентов

Компонент в Mithril определяется объектом с методами view и, опционально, oninit, oncreate, onupdate и onremove. Для unit-тестирования важно уметь:

  • Изолированно рендерить компонент в виртуальный DOM.
  • Проверять структуру DOM после рендера.
  • Имитировать пользовательские события.
  • Контролировать состояние компонента между рендерами.

Для тестирования чаще всего используют комбинацию Jest или Mocha с jsdom, который позволяет работать с DOM в Node.js.


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

Mithril предоставляет функцию m.mount и m.render для работы с DOM. В тестах обычно используют m.render, так как она позволяет рендерить в заданный контейнер и не зависит от жизненного цикла всего приложения.

Пример рендеринга:

import m from "mithril";

const Button = {
    view: ({attrs}) => m("button", {onclick: attrs.onclick}, attrs.label)
};

test("рендерит кнопку с правильным текстом", () => {
    const container = document.createElement("div");
    const handleClick = jest.fn();

    m.render(container, m(Button, {label: "Click me", onclick: handleClick}));

    const button = container.querySelector("button");
    expect(button.textContent).toBe("Click me");
});

Ключевые моменты:

  • Создание контейнера DOM в каждом тесте обеспечивает изоляцию.
  • Использование m.render вместо m.mount упрощает проверку структуры.
  • Jest mock-функции позволяют отслеживать вызовы обработчиков событий.

Проверка жизненного цикла компонентов

Для проверки методов жизненного цикла (oninit, oncreate, onupdate, onremove) в unit-тестах можно использовать шпионов и mock-функции:

const Component = {
    oninit: jest.fn(),
    oncreate: jest.fn(),
    view: () => m("div", "Content")
};

test("вызывает oninit и oncreate", () => {
    const container = document.createElement("div");
    m.render(container, m(Component));

    expect(Component.oninit).toHaveBeenCalled();
    expect(Component.oncreate).toHaveBeenCalled();
});

Особенности:

  • oninit вызывается до первого рендера, oncreateпосле вставки в DOM.
  • Для onupdate необходимо повторно вызывать m.render с новыми данными.

Тестирование событий и пользовательского взаимодействия

События в Mithril-приложениях легко эмулируются через стандартный API браузера:

test("клик вызывает обработчик", () => {
    const handleClick = jest.fn();
    const container = document.createElement("div");

    const Button = {
        view: ({attrs}) => m("button", {onclick: attrs.onclick}, "Click")
    };

    m.render(container, m(Button, {onclick: handleClick}));

    const button = container.querySelector("button");
    button.click();

    expect(handleClick).toHaveBeenCalledTimes(1);
});

Важные моменты:

  • Использование метода .click() эмулирует событие клика.
  • Jest позволяет проверить количество вызовов и параметры события.
  • Для сложных событий (keydown, input) используется new Event("eventType") с dispatchEvent.

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

Если компонент содержит локальное состояние через oninit или через замыкания, важно проверять изменение состояния при рендере и событиях:

const Counter = {
    count: 0,
    view: () => m("div",
        [
            m("span", `Count: ${Counter.count}`),
            m("button", {onclick: () => Counter.count++}, "Increment")
        ]
    )
};

test("увеличение счётчика", () => {
    const container = document.createElement("div");
    m.render(container, m(Counter));

    const button = container.querySelector("button");
    button.click();

    m.render(container, m(Counter)); // повторный рендер

    const span = container.querySelector("span");
    expect(span.textContent).toBe("Count: 1");
});

Особенности:

  • Повторный рендер необходим для отражения изменений состояния в DOM.
  • Для unit-тестов предпочтительно хранить состояние внутри компонента, а не глобально, чтобы избежать побочных эффектов.

Использование моков для внешних зависимостей

Для компонентов, которые делают HTTP-запросы или используют сторонние сервисы, применяется мокирование:

jest.mock("./api", () => ({
    fetchData: jest.fn().mockResolvedValue({name: "Mithril"})
}));

import {fetchData} from "./api";

const DataComponent = {
    data: null,
    oninit: async () => {
        DataComponent.data = await fetchData();
    },
    view: () => m("div", DataComponent.data?.name || "Loading")
};

test("рендерит данные после fetch", async () => {
    const container = document.createElement("div");
    await DataComponent.oninit();
    m.render(container, m(DataComponent));

    expect(container.textContent).toBe("Mithril");
});

Преимущества:

  • Изоляция тестов от реального API.
  • Управление результатами fetchData для проверки разных сценариев.
  • Возможность тестировать асинхронные компоненты без реальных сетевых запросов.

Интеграция с Jest и jsdom

Для полноценного тестирования компонентов Mithril рекомендуется использовать конфигурацию Jest с jsdom. Это позволяет:

  • Эмулировать браузерное окружение.
  • Проверять корректность вставки элементов в DOM.
  • Работать с событиями и стилями.

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

{
  "testEnvironment": "jsdom",
  "transform": {
    "^.+\\.js$": "babel-jest"
  }
}

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


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

  • Каждый компонент тестируется в отдельном контейнере DOM.
  • Избегать глобальных состояний между тестами.
  • Проверять не только рендер, но и реакцию на события.
  • Для асинхронных операций использовать async/await.
  • Сохранять тесты простыми, проверяя конкретное поведение компонента, а не реализацию фреймворка.