Cloudflare Workflows — это наша реализация концепции "Устойчивого выполнения". Они предоставляют бессерверный механизм на базе Платформы разработчика Cloudflare для создания долгоиграющих многоэтапных приложений, сохраняющих состояние при сбоях. Когда Workflows стали общедоступными ранее в этом году, они позволили разработчикам оркестрировать сложные процессы, которые было бы трудно или невозможно управлять с помощью традиционных бессостоятельных функций. Workflows управляют состоянием, повторными попытками и длительными ожиданиями, позволяя вам сосредоточиться на бизнес-логике.
Однако сложные оркестрации требуют надежного тестирования для обеспечения надежности. До сих пор тестирование Workflows было процессом "черного ящика". Хотя вы могли проверить, завершился ли экземпляр Workflow, через await его статуса, не было видимости промежуточных шагов. Это сильно затрудняло отладку. Успешен ли шаг обработки платежа? Получил ли шаг отправки подтверждающего письма правильные данные? Вы не могли быть уверены без проверки внешних систем или логов.
Почему это было необходимо?
Будучи сами разработчиками, мы понимаем необходимость обеспечения надежности кода и услышали ваши отзывы громко и ясно: опыт разработчика при тестировании Workflows нужно было улучшить.
Природа "черного ящика" при тестировании была одной частью проблемы. Однако помимо этого, ограниченное тестирование предлагалось по высокой цене. Если вы добавляли рабочий процесс в свой проект, даже если вы не тестировали его напрямую, вам требовалось отключить изолированное хранилище, потому что мы не могли гарантировать изоляцию между тестами. Изолированное хранилище — это функция vitest-pool-workers, гарантирующая, что каждый тест выполняется в чистой, предсказуемой среде, свободной от побочных эффектов других тестов. Принудительное его отключение означало, что состояние могло просачиваться между тестами, приводя к нестабильным, непредсказуемым и трудноотлаживаемым сбоям.
Это создавало трудный выбор для разработчиков, создающих сложные приложения. Если ваш проект использовал Workers, Durable Objects и R2 вместе с Workflows, вам приходилось либо отказываться от изолированного тестирования для всего проекта, либо пропускать тестирование. Это трение приводило к плохому опыту тестирования, что, в свою очередь, препятствовало внедрению Workflows. Решение этой проблемы было не просто улучшением, а критическим шагом в превращении Workflows частью любого хорошо тестируемого приложения Cloudflare.
Представляем изолированное тестирование для Workflows
Мы представляем новый набор API, который обеспечивает комплексное, детализированное и изолированное тестирование ваших Workflows, все работает локально и офлайн с vitest-pool-workers, нашей тестовой средой, поддерживающей запуск тестов в среде выполнения Workers workerd. Это позволяет выполнять быстрые, надежные и дешевые тестовые прогоны, не зависящие от сетевого подключения.
Они доступны через модуль cloudflare:test начиная с версии 0.9.0 и выше @cloudflare/vitest-pool-workers. Новый тестовый модуль предоставляет две основные функции для интроспекции ваших Workflows:
-
introspectWorkflowInstance: полезна для модульных тестов с известными идентификаторами экземпляров -
introspectWorkflow: полезна для интеграционных тестов, где идентификаторы обычно генерируются динамически.
Давайте рассмотрим практический пример.
Практический пример: тестирование рабочего процесса модерации блога
Представьте простой Workflow для модерации блога. Когда пользователь отправляет комментарий, Workflow запрашивает проверку у workers-ai. На основе возвращенного балла нарушения он затем ожидает, пока модератор одобрит или отклонит комментарий. Если комментарий одобрен, он вызывает step.do для публикации комментария через внешний API.
Тестирование этого без наших новых API было бы невозможно. У вас не было бы прямого способа имитировать результаты шага и симулировать одобрение модератора. Теперь вы можете имитировать все.
Вот тестовый код с использованием introspectWorkflowInstance с известным идентификатором экземпляра:
import { env, introspectWorkflowInstance } from "cloudflare:test";
it("должен имитировать неоднозначный балл, одобрить комментарий и завершиться", async () => {
// КОНФИГУРАЦИЯ
await using instance = await introspectWorkflowInstance(
env.MODERATOR,
"my-workflow-instance-id-123"
);
await instance.modify(async (m) => {
await m.mockStepResult({ name: "AI content scan" }, { violationScore: 50 });
await m.mockEvent({
type: "moderation-approval",
payload: { action: "approved" },
});
await m.mockStepResult({ name: "publish comment" }, { status: "published" });
});
await env.MODERATOR.create({ id: "my-workflow-instance-id-123" });
// УТВЕРЖДЕНИЯ
expect(await instance.waitForStepResult({ name: "AI content scan" })).toEqual(
{ violationScore: 50 }
);
expect(
await instance.waitForStepResult({ name: "publish comment" })
).toEqual({ status: "published" });
await expect(instance.waitForStatus("complete")).resolves.not.toThrow();
});
Этот тест имитирует результаты шагов, требующих вызовов внешних API, таких как 'AI content scan', который вызывает Workers AI, и шаг 'publish comment', который вызывает внешний API блога.
Если идентификатор экземпляра неизвестен, потому что вы делаете запрос к воркеру, который запускает один/несколько экземпляров Workflow со случайно сгенерированными идентификаторами, вы можете вызвать introspectWorkflow(env.MY_WORKFLOW). Вот тестовый код для этого сценария, где создается только один экземпляр Workflow:
it("workflow должен имитировать балл без нарушений и быть успешным", async () => {
// КОНФИГУРАЦИЯ
await using introspector = await introspectWorkflow(env.MODERATOR);
await introspector.modifyAll(async (m) => {
await m.disableSleeps();
await m.mockStepResult({ name: "AI content scan" }, { violationScore: 0 });
});
await SELF.fetch(`https://mock-worker.local/moderate`);
const instances = introspector.get();
expect(instances.length).toBe(1);
// УТВЕРЖДЕНИЯ
const instance = instances[0];
expect(await instance.waitForStepResult({ name: "AI content scan" })).toEqual({ violationScore: 0 });
await expect(instance.waitForStatus("complete")).resolves.not.toThrow();
});
Обратите внимание, как в обоих примерах мы вызываем интроспекторы с await using — это синтаксис Явного управления ресурсами из современного JavaScript. Это крайне важно здесь, потому что когда объекты-интроспекторы выходят из области видимости в конце теста, их метод удаления автоматически вызывается. Именно так мы обеспечиваем, чтобы каждый тест работал со своим собственным изолированным хранилищем.
Функции modify и modifyAll являются шлюзом для управления экземплярами. Внутри его обратного вызова вы получаете доступ к объекту-модификатору с методами для внедрения поведения, такого как имитация результатов шагов, событий и отключение ожиданий.
Подробную документацию вы можете найти в Документации Cloudflare для Workers.
Как мы подключили Vitest к механизму Workflows
Чтобы понять решение, вам сначала нужно понять локальную архитектуру. Когда вы запускаете wrangler dev, ваши Workflows работают на базе Miniflare, симулятора для тестирования Cloudflare Workers, и workerd. Каждый запущенный экземпляр рабочего процесса поддерживается своим собственным SQLite Durable Object, который мы называем "Engine DO". Этот Engine DO отвечает за выполнение шагов, сохранение состояния и управление жизненным циклом экземпляра. Он находится внутри локальной изолированной среды выполнения Workers.
Между тем, запускатель тестов Vitest — это отдельный процесс Node.js, находящийся вне workerd. Именно поэтому у нас есть пользовательский пул Vitest, который позволяет тестам запускаться внутри workerd, называемый vitest-pool-workers. Vitest-pool-workers имеет Runner Worker, который является воркером для запуска тестов с привязками ко всему, указанному в файле wrangler.json пользователя. Этот воркер имеет доступ к API под модулем "cloudflare:test". Он общается с Node.js через специальный DO под названием Runner Object через WebSocket/RPC.
Первый подход, который мы рассмотрели, заключался в использовании воркера-запускателя тестов. В своем текущем состоянии Runner worker имеет доступ к привязкам Workflow из Workflows, определенных в файле wrangler. Мы также рассмотрели возможность привязки пространства имен Engine DO каждого Workflow к этому воркеру-запускателю. Это дало бы vitest-pool-workers прямой доступ к Engine DO, где можно было бы напрямую вызывать методы Engine.
Хотя и многообещающий, этот подход потребовал бы нежелательных изменений в ядре Miniflare и vitest-pool-workers, делая его слишком инвазивным для этой единственной функции.
Во-первых, нам потребовалось бы добавить новое поле unsafe в Объекты Durable Miniflare. Его единственной целью было бы указание имени сервиса наших Движков, чтобы предотвратить применение Miniflare своего пользовательского префикса по умолчанию, который в противном случае помешал бы найти Объекты Durable.
Во-вторых, vitest-pool-workers был бы вынужден привязать каждый Движок DO из Workflows в проекте к своему раннеру, даже те, которые не тестируются. Это ввело бы нежелательные привязки в тестовую среду, потребовав дополнительной очистки, чтобы гарантировать, что они не будут доступны в окружении тестов пользователя.
Прорыв
Решение — это комбинация привилегированных API, доступных только локально, и Вызовов Удаленных Процедур (RPC).
Сначала мы добавили набор unsafe-функций в локальную реализацию привязки Workflows — функции, которые недоступны в рабочей среде. Они действуют как контролируемая точка доступа, доступная из тестового окружения, позволяя тестовому раннеру получить заглушку для конкретного Движка DO, указав его идентификатор экземпляра.
Как только тестовый раннер получает эту заглушку, он использует RPC для вызова конкретных, доверенных методов на Движке DO через специальный RpcTarget под названием WorkflowInstanceModifier. Любой класс, расширяющий RpcTarget, имеет свои объекты замененными на заглушку. Вызов метода на этой заглушке, в свою очередь, совершает RPC-вызов обратно к исходному объекту.
Этот более простой подход гораздо менее инвазивен, потому что он ограничен средой Workflows, что также гарантирует безопасную изоляцию любых будущих изменений функциональности.
Интроспекция Workflows с неизвестными ID
При создании экземпляров Workflows (через create() или createBatch()) разработчики могут указать конкретный ID или позволить ему сгенерироваться автоматически. Этот ID идентифицирует экземпляр Workflow и затем используется для создания связанного ID Движка DO.
Логичной отправной точкой для реализации был introspectWorkflowInstance(binding, instanceID), так как ID экземпляра известен заранее. Это позволяет нам сгенерировать ID Движка DO, необходимый для идентификации движка, связанного с этим экземпляром Workflow.
Но часто одна часть вашего приложения (например, HTTP-эндпоинт) создает экземпляр Workflow со случайно сгенерированным ID. Как мы можем провести интроспекцию экземпляра, если мы не знаем его ID до тех пор, пока он не создан?
Ответом стало использование мощной возможности JavaScript: объектов Proxy.
Когда вы используете introspectWorkflow(binding), мы оборачиваем привязку Workflow в Proxy. Этот прокси ненавязчиво перехватывает все вызовы к привязке, специально отслеживая вызовы .create() и .createBatch(). Когда ваш тест инициирует создание workflow, прокси анализирует вызов. Он захватывает ID экземпляра — либо предоставленный вами, либо случайно сгенерированный — и немедленно настраивает интроспекцию для этого ID, применяя все модификации, которые вы определили в вызове modifyAll. Исходный вызов создания затем продолжается как обычно.
env[workflow] = new Proxy(env[workflow], {
get(target, prop) {
if (prop === "create") {
return new Proxy(target.create, {
async apply(_fn, _this, [opts = {}]) {
// 1. Гарантируем существование ID
const optsWithId = "id" in opts ? opts : { id: crypto.randomUUID(), ...opts };
// 2. Применяем тестовые модификации перед созданием
await introspectAndModifyInstance(optsWithId.id);
// 3. Вызываем исходный метод 'create'
return target.create(optsWithId);
},
});
}
// Та же логика для createBatch()
}
}
Когда блок await using из introspectWorkflow() завершается, или метод dispose() вызывается в конце теста, интроспектор уничтожается, а прокси удаляется, возвращая привязку в исходное состояние. Это малоинвазивный подход, который ставит во главу угла опыт разработчика и долгосрочную поддерживаемость.
Начните тестировать Workflows
Готовы добавить тесты к вашим Workflows? Вот как начать:
-
Обновите зависимости: Убедитесь, что вы используете
@cloudflare/vitest-pool-workersверсии 0.9.0 или новее. Выполните следующую команду в вашем проекте:npm install @cloudflare/vitest-pool-workers@latest -
Настройте тестовое окружение: Если вы новичок в тестировании на Workers, следуйте нашему руководству по написанию первого теста.