← Cloudflare Workers / workers / testing / vitest-integration
Test APIs
Интеграция Workers Vitest предоставляет вспомогательные функции времени выполнения для написания тестов. Некоторые из них экспортируются из cloudflare:workers модуль, а также другие из cloudflare:test модуль. Оба модуля предоставляются @cloudflare/vitest-plugin пакет, но его можно импортировать только из тестовых файлов, которые выполняются в среде выполнения Workers.
cloudflare:workers экспорты
-
env: import("cloudflare:workers").ProvidedEnv-
Предоставляет
envобъект для использования в качестве второго аргумента, передаваемого экспортируемым обработчикам в формате ES modules. Это даёт доступ к привязки который вы определили в Файл конфигурации Vitest.
import { env } from "cloudflare:workers"; it("uses binding", async () => { await env.KV_NAMESPACE.put("key", "value"); expect(await env.KV_NAMESPACE.get("key")).toBe("value"); });Чтобы задать тип этого значения, используйте ambient module type:
declare module "cloudflare:workers" { interface ProvidedEnv { KV_NAMESPACE: KVNamespace; } // ...or if you have an existing `Env` type... interface ProvidedEnv extends Env {} }
-
-
exports: объект-
Предоставляет доступ к экспортам
mainWorker. Используйтеexports.default.fetch()для написания интеграционных тестов для обработчика экспорта по умолчанию вашего Worker.mainWorker выполняется в том же isolate/context, что и тесты, поэтому любые глобальные моки также будут к нему применяться. В отличие от предыдущегоSELFпривязку,exportsне предоставляет доступ к Assets. Чтобы протестировать ресурсы, используйтеstartDevWorker().
import { exports } from "cloudflare:workers"; it("dispatches fetch event", async () => { const response = await exports.default.fetch("https://example.com"); expect(await response.text()).toMatchInlineSnapshot(...); });
-
cloudflare:test экспорты
События
-
createExecutionContext(): ExecutionContext- Создает экземпляр
contextобъект для использования в качестве третьего аргумента для экспортируемых обработчиков в формате ES modules.
- Создает экземпляр
-
waitOnExecutionContext(ctx:ExecutionContext): Promise<void>-
Используйте это, чтобы дождаться выполнения всех Promise, переданных в
ctx.waitUntil()до завершения, прежде чем выполнять проверки побочных эффектов в тестах. Принимает только экземплярыExecutionContextвозвращаемыйcreateExecutionContext().
import { env } from "cloudflare:workers"; import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; import { it, expect } from "vitest"; import worker from "./index.mjs"; it("calls fetch handler", async () => { const request = new Request("https://example.com"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); expect(await response.text()).toMatchInlineSnapshot(...); });
-
-
createScheduledController(options?:FetcherScheduledOptions): ScheduledController-
Создает экземпляр
ScheduledControllerдля использования в качестве первого аргумента для modules-formatscheduled()экспортированных обработчиков.
import { env } from "cloudflare:workers"; import { createScheduledController, createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; import { it, expect } from "vitest"; import worker from "./index.mjs"; it("calls scheduled handler", async () => { const ctrl = createScheduledController({ scheduledTime: new Date(1000), cron: "30 * * * *" }); const ctx = createExecutionContext(); await worker.scheduled(ctrl, env, ctx); await waitOnExecutionContext(ctx); });
-
-
createMessageBatch(queueName:string, messages:ServiceBindingQueueMessage[]): MessageBatch- Создает экземпляр
MessageBatchдля использования в качестве первого аргумента для modules-formatqueue()экспортированных обработчиков.
- Создает экземпляр
-
getQueueResult(batch:MessageBatch, ctx:ExecutionContext): Promise<FetcherQueueResult>-
Возвращает состояние подтверждения и повтора сообщений в
MessageBatch, и ожидает завершения всехExecutionContext#waitUntil(), то есть переданныеPromises для завершения. Принимает только экземплярыMessageBatchвозвращаемыйcreateMessageBatch(), и экземплярыExecutionContextвозвращаемыйcreateExecutionContext().
import { env } from "cloudflare:workers"; import { createMessageBatch, createExecutionContext, getQueueResult } from "cloudflare:test"; import { it, expect } from "vitest"; import worker from "./index.mjs"; it("calls queue handler", async () => { const batch = createMessageBatch("my-queue", [ { id: "message-1", timestamp: new Date(1000), body: "body-1" } ]); const ctx = createExecutionContext(); await worker.queue(batch, env, ctx); const result = await getQueueResult(batch, ctx); expect(result.ackAll).toBe(false); expect(result.retryBatch).toMatchObject({ retry: false }); expect(result.explicitAcks).toStrictEqual(["message-1"]); expect(result.retryMessages).toStrictEqual([]); });
-
Durable Objects
-
runInDurableObject<O extends DurableObject, R>(stub:DurableObjectStub, callback:(instance: O, state: DurableObjectState) => R | Promise<R>): Promise<R>-
Выполняет предоставленный
callbackвнутри Durable Object, соответствующего указанномуstub.
Это временно заменяет для вашего Durable Object
fetch()обработчик сcallback, а затем отправляет ему запрос и возвращает результат. Это можно использовать для вызова методов Durable Object или наблюдения за ними, а также для записи или получения сохранённых данных. Обратите внимание, что это можно использовать только сstubs, указывающие на Durable Objects, определенные вmainWorker.
export class Counter { constructor(readonly state: DurableObjectState) {} async fetch(request: Request): Promise<Response> { let count = (await this.state.storage.get<number>("count")) ?? 0; void this.state.storage.put("count", ++count); return new Response(count.toString()); } }import { env } from "cloudflare:workers"; import { runInDurableObject } from "cloudflare:test"; import { it, expect } from "vitest"; import { Counter } from "./index.ts"; it("increments count", async () => { const id = env.COUNTER.newUniqueId(); const stub = env.COUNTER.get(id); let response = await stub.fetch("https://example.com"); expect(await response.text()).toBe("1"); response = await runInDurableObject(stub, async (instance: Counter, state) => { expect(instance).toBeInstanceOf(Counter); expect(await state.storage.get<number>("count")).toBe(1); const request = new Request("https://example.com"); return instance.fetch(request); }); expect(await response.text()).toBe("2"); });
-
-
runDurableObjectAlarm(stub:DurableObjectStub): Promise<boolean>- Немедленно запускает и удаляет Durable Object, на который указывает
stub: alarm, если он запланирован. Возвращаетtrueесли alarm сработал, иfalseв противном случае. Обратите внимание, что это можно использовать только сstubs, указывающие на Durable Objects, определенные вmainWorker.
- Немедленно запускает и удаляет Durable Object, на который указывает
-
evictDurableObject(stub:DurableObjectStub, options?:DurableObjectEvictionOptions): Promise<void>-
Вытесняет выполняемый в данный момент Durable Object, на который указывает
stub, разрушая его экземпляр, чтобы сбросить состояние в памяти. По умолчанию гибернируемые WebSocket переводятся в спящий режим, а не закрываются, и вытеснение ожидает завершения выполняющихся запросов до 30 секунд.
Полезно для проверки поведения Durable Object при вытеснении из памяти, например при восстановлении состояния из хранилища или возобновлении гибернированных WebSocket-соединений.
Отклоняется, если
stubне является стабом Durable Object, если целевой Durable Object в данный момент не запущен, либо если для его пространства имен отключено вытеснение. Обратите внимание, что это можно использовать только сstubs, указывающие на Durable Objects, определенные вmainWorker.
import { env } from "cloudflare:workers"; import { evictDurableObject } from "cloudflare:test"; import { it, expect } from "vitest"; it("preserves stored data across eviction", async () => { const id = env.COUNTER.idFromName("evict-test"); const stub = env.COUNTER.get(id); // Each request increments and persists the count to storage expect(await (await stub.fetch("https://example.com")).text()).toBe("1"); expect(await (await stub.fetch("https://example.com")).text()).toBe("2"); // Evict the Durable Object. The in-memory instance is torn down, // but durable storage is preserved. await evictDurableObject(stub); // The next request reconstructs the instance and reads the persisted count expect(await (await stub.fetch("https://example.com")).text()).toBe("3"); }); -
DurableObjectEvictionOptionsинтерфейс управляет поведением вытеснения:Свойство Тип По умолчанию Описание webSockets"close" | "hibernate""hibernate"Управляет тем, что происходит с WebSocket-соединениями, поддерживающими гибернацию, при вытеснении Durable Object. При "hibernate", WebSocket-соединения переходят в режим гибернации и могут возобновляться после вытеснения. С"close", WebSocket-соединения закрываются.
-
-
listDurableObjectIds(namespace:DurableObjectNamespace): Promise<DurableObjectId[]>-
Возвращает идентификаторы всех объектов, созданных в
namespace. Учитывает изоляцию хранилища по файлам, то есть объекты, созданные в другом тестовом файле, не будут возвращены.
import { env } from "cloudflare:workers"; import { listDurableObjectIds } from "cloudflare:test"; import { it, expect } from "vitest"; it("increments count", async () => { const id = env.COUNTER.newUniqueId(); const stub = env.COUNTER.get(id); const response = await stub.fetch("https://example.com"); expect(await response.text()).toBe("1"); const ids = await listDurableObjectIds(env.COUNTER); expect(ids.length).toBe(1); expect(ids[0].equals(id)).toBe(true); });
-
-
reset(): Promise<void>-
Удаляет все данные из всех подключенных привязок. Это полезно для сброса состояния между тестовыми блоками.
import { reset } from "cloudflare:test"; import { afterEach } from "vitest"; afterEach(async () => { await reset(); });
-
-
abortAllDurableObjects(): Promise<void>-
Сбрасывает все экземпляры Durable Object. В отличие от
reset(), это не удаляет сохранённые данные. Такая операция принудительно останавливает все запущенные экземпляры Durable Object, отбрасывая состояние в памяти и не дожидаясь завершения выполняющихся запросов.
import { abortAllDurableObjects } from "cloudflare:test"; import { afterEach } from "vitest"; afterEach(async () => { await abortAllDurableObjects(); });
-
-
evictAllDurableObjects(options?:DurableObjectEvictionOptions): Promise<void>-
Вытесняет все выполняемые в данный момент Durable Object в пространствах имён, поддерживающих вытеснение. В отличие от
abortAllDurableObjects(), вытеснение происходит плавно: WebSocket-соединения с поддержкой hibernation по умолчанию переводятся в спящий режим, а не закрываются, и вытеснение ожидает до 30 секунд для завершения выполняющихся запросов. Состояние в памяти сбрасывается путём остановки каждого экземпляра.
Неактивные или простаивающие Durable Objects пропускаются, а пространства имён с запретом вытеснения учитываются. Принимает те же
DurableObjectEvictionOptionsкакevictDurableObject().
import { evictAllDurableObjects } from "cloudflare:test"; import { afterEach } from "vitest"; afterEach(async () => { await evictAllDurableObjects(); });
-
D1
-
applyD1Migrations(db:D1Database, migrations:D1Migration[], migrationTableName?:string): Promise<void>- Применяет все неприменённые Миграции D1 хранится в
migrationsмассив к базе данныхdb, записывая состояние миграций вmigrationsTableNameтаблица.migrationsTableNameпо умолчанию равноd1_migrations. ВызовитеreadD1Migrations()функцию из@cloudflare/vitest-plugin/configпакет внутри Node.js, чтобы получитьmigrationsмассив. См. Рецепт D1 ↗ пример проекта с использованием миграций.
- Применяет все неприменённые Миграции D1 хранится в
Workflows
-
introspectWorkflowInstance(workflow: Workflow, instanceId: string): Promise<WorkflowInstanceIntrospector>-
Создает интроспектор для конкретного экземпляра Workflow, используемый для изменить его поведение, await результаты, и очистить его состояние во время тестов. Это основная точка входа для тестирования отдельных экземпляров Workflow с известным ID.
import { env } from "cloudflare:workers"; import { introspectWorkflowInstance } from "cloudflare:test"; it("should disable all sleeps, mock an event and complete", async () => { // 1. CONFIGURATION await using instance = await introspectWorkflowInstance(env.MY_WORKFLOW, "123456"); await instance.modify(async (m) => { await m.disableSleeps(); await m.mockEvent({ type: "user-approval", payload: { approved: true, approverId: "user-123" }, }); }); // 2. EXECUTION await env.MY_WORKFLOW.create({ id: "123456" }); // 3. ASSERTION await expect(instance.waitForStatus("complete")).resolves.not.toThrow(); const output = await instance.getOutput(); expect(output).toEqual({ success: true }); // 4. DISPOSE: is implicit and automatic here. }); -
Возвращённый
WorkflowInstanceIntrospectorобъект имеет следующие методы:modify(fn: (m: WorkflowInstanceModifier) => Promise<void>): Promise<void>: Применяет изменения к поведению экземпляра Workflow.waitForStepResult(step: { name: string; index?: number }): Promise<unknown>: Ожидает завершения указанного шага и возвращает результат. Если несколько шагов имеют одинаковое имя, используйте необязательныйindexсвойство (нумерация с 1, по умолчанию1) чтобы указать конкретное вхождение.waitForStatus(status: InstanceStatus["status"]): Promise<void>: Ожидает достижения экземпляром Workflow определённого статус (например, 'running', 'complete').getOutput(): Promise<unknown>: Возвращает результат успешно завершённого экземпляра Workflow.getError(): Promise<{name: string, message: string}>: Возвращает информацию об ошибке для экземпляра Workflow, завершившегося с ошибкой. Информация об ошибке имеет следующий формат:{ name: string; message: string }.dispose(): Promise<void>: Уничтожает экземпляр Workflow, что критично для изоляции тестов. Если эта функция не вызвана иawait usingне используется, изолированное хранилище не будет работать, и состояние экземпляра сохранится между последующими тестами. Например, экземпляр, который стал завершенным в одном тесте, уже будет завершенным в начале следующего.[Symbol.asyncDispose](): Promise<void>: Обеспечивает автоматический dispose. Вызывается черезawait usingинструкция, которая вызываетdispose().
-
-
introspectWorkflow(workflow: Workflow): Promise<WorkflowIntrospector>-
Создает интроспектор для Workflow, если идентификаторы экземпляров заранее неизвестны. Это позволяет задавать изменения, которые будут применяться к все экземпляры, созданные впоследствии.
import { env, exports } from "cloudflare:workers"; import { introspectWorkflow } from "cloudflare:test"; it("should disable all sleeps, mock an event and complete", async () => { // 1. CONFIGURATION await using introspector = await introspectWorkflow(env.MY_WORKFLOW); await introspector.modifyAll(async (m) => { await m.disableSleeps(); await m.mockEvent({ type: "user-approval", payload: { approved: true, approverId: "user-123" }, }); }); // 2. EXECUTION await env.MY_WORKFLOW.create(); // 3. ASSERTION const instances = introspector.get(); for(const instance of instances) { await expect(instance.waitForStatus("complete")).resolves.not.toThrow(); const output = await instance.getOutput(); expect(output).toEqual({ success: true }); } // 4. DISPOSE: is implicit and automatic here. });Экземпляр workflow не обязательно создавать непосредственно внутри теста. Introspector перехватит all экземпляры, созданные после его инициализации. Например, можно инициировать создание один или несколько экземпляров через один
fetchсобытие в свой Worker:// This also works for the EXECUTION phase: await exports.default.fetch("https://example.com/trigger-workflows"); -
Возвращённый
WorkflowIntrospectorобъект имеет следующие методы:modifyAll(fn: (m: WorkflowInstanceModifier) => Promise<void>): Promise<void>: Применяет изменения ко всем экземплярам Workflow, созданным после вызоваintrospectWorkflow.get(): Promise<WorkflowInstanceIntrospector[]>: Возвращает всеWorkflowInstanceIntrospectorобъекты из экземпляров, созданных послеintrospectWorkflowбыл вызван.dispose(): Promise<void>: Уничтожает Workflow introspector. ВсеWorkflowInstanceIntrospectorот созданных экземпляров также будут уничтожены. Это важно, чтобы изменения и захваченные экземпляры не переходили между тестами. После вызова этого методаWorkflowIntrospectorне следует использовать повторно.[Symbol.asyncDispose](): Promise<void>: Обеспечивает автоматический dispose. Вызывается черезawait usingинструкция, которая вызываетdispose().
-
-
WorkflowInstanceModifier-
Этот объект передается в
modifyиmodifyAllобратные вызовы для имитации или изменения поведения шагов, событий и пауз экземпляра Workflow.disableSleeps(steps?: { name: string; index?: number }[]): Отключает паузы (sleep), из-за чегоstep.sleep()иstep.sleepUntil()разрешиться немедленно. Еслиstepsопущен, все паузы отключаются.disableRetryDelays(steps?: { name: string; index?: number }[]): Отключает задержки повторных попыток (backoff), из-за чего повторные попытки для неудачногоstep.do()чтобы выполнить немедленно, без ожидания. Повторные попытки всё равно происходят, только пауза между ними убирается. Еслиstepsопущен, все задержки повтора отключаются.mockStepResult(step: { name: string; index?: number }, stepResult: unknown): Имитирует результатstep.do(), из-за чего он мгновенно возвращает указанное значение, не выполняя реализацию шага.mockStepError(step: { name: string; index?: number }, error: Error, times?: number): Принудительно вызываетstep.do()выбрасывать ошибку, имитируя сбой.timesпредставляет собой необязательное число, задающее, сколько раз шаг должен завершиться ошибкой. Еслиtimesопущен, шаг будет завершаться ошибкой при каждой попытке, из-за чего экземпляр Workflow завершится сбоем.forceStepTimeout(step: { name: string; index?: number }, times?: number): Принудительно вызываетstep.do()чтобы завершаться ошибкой из-за немедленного тайм-аута.timesпредставляет собой необязательное число, задающее, сколько раз для шага должен истечь тайм-аут. Еслиtimesопущен, шаг будет превышать тайм-аут при каждой попытке, из-за чего экземпляр Workflow завершится сбоем.mockEvent(event: { type: string; payload: unknown }): Отправляет мок-событие в экземпляр Workflow, вызываяstep.waitForEvent()разрешиться с переданными данными.typeдолжен совпадать сwaitForEventтип.forceEventTimeout(step: { name: string; index?: number }): Принудительно вызываетstep.waitForEvent()немедленно завершаться по тайм-ауту, из-за чего шаг завершится неудачей.
import { env } from "cloudflare:workers"; import { introspectWorkflowInstance } from "cloudflare:test"; // This example showcases explicit disposal it("should apply all modifier functions", async () => { // 1. CONFIGURATION const instance = await introspectWorkflowInstance(env.COMPLEX_WORKFLOW, "123456"); try { // Modify instance behavior await instance.modify(async (m) => { // Disables all sleeps to make the test run instantly await m.disableSleeps(); // Disables retry backoff delays so retries execute without waiting await m.disableRetryDelays(); // Mocks the successful result of a data-fetching step await m.mockStepResult( { name: "get-order-details" }, { orderId: "abc-123", amount: 99.99 } ); // Mocks an incoming event to satisfy a `step.waitForEvent()` await m.mockEvent({ type: "user-approval", payload: { approved: true, approverId: "user-123" }, }); // Forces a step to fail once with a specific error to test retry logic await m.mockStepError( { name: "process-payment" }, new Error("Payment gateway timeout"), 1 // Fail only the first time ); // Forces a `step.do()` to time out immediately await m.forceStepTimeout({ name: "notify-shipping-partner" }); // Forces a `step.waitForEvent()` to time out await m.forceEventTimeout({ name: "wait-for-fraud-check" }); }); // 2. EXECUTION await env.COMPLEX_WORKFLOW.create({ id: "123456" }); // 3. ASSERTION expect(await instance.waitForStepResult({ name: "get-order-details" })).toEqual({ orderId: "abc-123", amount: 99.99, }); // Given the forced timeouts, the workflow will end in an errored state await expect(instance.waitForStatus("errored")).resolves.not.toThrow(); const error = await instance.getError(); expect(error.name).toEqual("Error"); expect(error.message).toContain("Execution timed out"); } catch { // 4. DISPOSE await instance.dispose(); } });Чтобы обратиться к конкретному шагу, используйте его
name. Если несколько шагов имеют одинаковое имя, используйте необязательныйindexсвойство (нумерация с 1, по умолчанию1) чтобы указать конкретное вхождение.
-