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

Асинхронность является неотъемлемой частью современных веб-приложений. В Mithril асинхронные операции чаще всего связаны с HTTP-запросами, таймерами и обработкой событий, что делает тестирование критически важным для стабильности приложения. Важной особенностью является то, что Mithril предоставляет собственный m.request, который возвращает промис, и корректное тестирование требует понимания работы промисов и механизма виртуального DOM.


Модуль m.request и промисы

m.request используется для выполнения HTTP-запросов. Он возвращает объект, совместимый с промисами:

m.request({
    method: "GET",
    url: "/api/data"
}).then(response => {
    console.log(response);
});

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


Использование async/await в тестах

Современные тестовые фреймворки (Jest, Mocha, Ava) позволяют использовать async/await, что упрощает работу с асинхронными операциями:

import m from "mithril";

test("загрузка данных с сервера", async () => {
    const data = await m.request({ method: "GET", url: "/api/data" });
    expect(data).toHaveProperty("id");
});

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


Мокаут HTTP-запросов

Для тестирования без реального сервера используется мокаут сетевых вызовов. В Jest это реализуется через jest.mock или библиотеки типа fetch-mock:

import m from "mithril";
import fetchMock from "fetch-mock";

afterEach(() => fetchMock.restore());

test("получение данных через моки", async () => {
    fetchMock.get("/api/data", { id: 1, name: "test" });

    const result = await m.request({ method: "GET", url: "/api/data" });
    expect(result.id).toBe(1);
});

Преимущество мокаутов — тесты становятся детерминированными и не зависят от внешних сервисов.


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

Mithril компоненты могут выполнять асинхронные действия при инициализации или в методах жизненного цикла (oninit, oncreate):

const Component = {
    data: null,
    oninit: async function() {
        this.data = await m.request({ method: "GET", url: "/api/data" });
    },
    view: function() {
        return m("div", this.data ? this.data.name : "Загрузка...");
    }
};

Тестирование такого компонента требует ожидания завершения асинхронного кода перед проверкой состояния DOM:

import { render, screen } from "@testing-library/dom";
import m from "mithril";

test("компонент отображает данные после загрузки", async () => {
    fetchMock.get("/api/data", { name: "Mithril" });

    const root = document.createElement("div");
    m.mount(root, Component);

    // Подождать, пока асинхронный код выполнится
    await new Promise(resolve => setTimeout(resolve, 0));

    expect(root.textContent).toContain("Mithril");
});

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


Обработка ошибок и тестирование отказов

Асинхронные операции могут завершаться ошибкой. Для тестирования сценариев ошибок нужно использовать конструкцию try/catch или методы тестового фреймворка для проверки исключений:

test("обработка ошибки запроса", async () => {
    fetchMock.get("/api/data", 500);

    let error;
    try {
        await m.request({ method: "GET", url: "/api/data" });
    } catch (e) {
        error = e;
    }

    expect(error).toBeDefined();
});

Важно проверять правильность обработки ошибок в компоненте, чтобы UI не ломался при неудачных запросах.


Моки таймеров и асинхронных событий

В Mithril асинхронные события могут использовать setTimeout или setInterval. Для тестирования удобно использовать фиктивные таймеры (Jest jest.useFakeTimers()):

jest.useFakeTimers();

test("таймер обновляет данные", () => {
    const component = {
        count: 0,
        oncreate: function() {
            setInterval(() => this.count++, 1000);
        },
        view: function() {
            return m("span", this.count);
        }
    };

    const root = document.createElement("div");
    m.mount(root, component);

    jest.advanceTimersByTime(3000);
    expect(component.count).toBe(3);
});

Использование фиктивных таймеров позволяет контролировать ход времени и мгновенно проверять асинхронные действия без реальных задержек.


Взаимодействие с виртуальным DOM

Mithril применяет виртуальный DOM, и асинхронные обновления должны корректно отражаться на отображении. В тестах важно перемонтировать компонент или использовать m.redraw.sync() после изменения состояния:

component.data = { name: "test" };
m.redraw.sync();
expect(root.textContent).toContain("test");

Это гарантирует, что все асинхронные изменения состояния корректно отображены в DOM до проверки.


Практические рекомендации

  • Всегда использовать async/await или явное ожидание промисов.
  • Мокаутить сетевые запросы, чтобы тесты были независимы от внешних сервисов.
  • Для таймеров применять фиктивные таймеры, чтобы ускорить тесты.
  • Проверять как успешные, так и ошибочные сценарии асинхронных операций.
  • После асинхронных обновлений использовать m.redraw.sync() для синхронизации с виртуальным DOM.
  • Разделять логику асинхронных действий и отображения для облегчения тестирования компонентов.