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

Stimulus — это легковесный JavaScript-фреймворк, который позволяет связывать поведение с элементами DOM через контроллеры. В реальных приложениях контроллеры часто взаимодействуют с асинхронными процессами: сетевыми запросами, таймерами, WebSocket и другими событиями. Тестирование таких сценариев требует особого подхода, чтобы корректно проверять результаты и состояние интерфейса после завершения асинхронных операций.

Асинхронные действия в контроллерах

В Stimulus контроллер может содержать методы, выполняющие асинхронные действия. Пример типичного сценария:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["output"]

  async fetchData() {
    this.outputTarget.textContent = "Загрузка..."
    const response = await fetch("/api/data")
    const data = await response.json()
    this.outputTarget.textContent = data.message
  }
}

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

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

Инструменты для тестирования

Для тестирования Stimulus-контроллеров используют Jest, Mocha, или другие фреймворки, поддерживающие асинхронные тесты. Важные аспекты:

  1. JSDOM — позволяет эмулировать DOM в тестах без браузера.
  2. fetch-mock или MSW — позволяют имитировать сетевые запросы.
  3. jest.useFakeTimers() — полезно для проверки таймеров или дебаунсов.

Пример асинхронного теста

Тестирование метода fetchData может выглядеть следующим образом:

import { Application } from "@hotwired/stimulus"
import DataController from "./data_controller"

global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ message: "Привет, мир!" })
  })
)

describe("DataController", () => {
  let application, element

  beforeEach(() => {
    application = Application.start()
    element = document.createElement("div")
    element.innerHTML = `<span data-target="data.output"></span>`
    application.register("data", DataController)
    document.body.appendChild(element)
  })

  afterEach(() => {
    application.stop()
    document.body.innerHTML = ""
    jest.clearAllMocks()
  })

  test("обновляет содержимое после fetch", async () => {
    const controller = application.getControllerForElementAndIdentifier(element, "data")
    
    await controller.fetchData()

    expect(element.querySelector("[data-target='data.output']").textContent).toBe("Привет, мир!")
    expect(fetch).toHaveBeenCalledWith("/api/data")
  })
})

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

  • Использование await гарантирует, что тест дождется завершения асинхронной операции.
  • Мокирование fetch предотвращает реальные HTTP-запросы.
  • Очистка моков и DOM после каждого теста предотвращает побочные эффекты.

Тестирование таймеров и задержек

Контроллеры часто используют setTimeout или setInterval. Jest позволяет управлять временем с помощью fake timers:

jest.useFakeTimers()

test("показывает сообщение после задержки", () => {
  const element = document.createElement("div")
  element.innerHTML = `<span data-target="data.output"></span>`
  const controller = new DataController(element, {})

  controller.delayedMessage()

  expect(element.querySelector("[data-target='data.output']").textContent).toBe("")

  jest.advanceTimersByTime(1000)

  expect(element.querySelector("[data-target='data.output']").textContent).toBe("Готово")
})

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

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

Мока и шпионы на методы

Иногда необходимо убедиться, что определенный метод контроллера вызван после асинхронного события. Jest предоставляет возможность создавать шпионы (jest.spyOn):

test("вызывает метод обработки данных после fetch", async () => {
  const controller = application.getControllerForElementAndIdentifier(element, "data")
  const spy = jest.spyOn(controller, "processData")

  await controller.fetchData()

  expect(spy).toHaveBeenCalledWith({ message: "Привет, мир!" })
})

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

Общие рекомендации

  • Всегда использовать await или возвращать промис в тестах.
  • Мокировать внешние зависимости: fetch, axios, WebSocket.
  • Использовать fake timers для контроля задержек и интервалов.
  • Очистка DOM и моков после каждого теста предотвращает неожиданные побочные эффекты.
  • Проверять не только результат, но и промежуточное состояние интерфейса.

Асинхронное тестирование в Stimulus позволяет точно моделировать пользовательские сценарии и гарантировать стабильность поведения интерфейса при взаимодействии с внешними ресурсами. Оно обеспечивает уверенность, что контроллер корректно обновляет DOM и вызывает необходимые методы в нужный момент.