Implement Vitest testing environment for Miniflare 3 - #4795
Conversation
🦋 Changeset detectedLatest commit: d8be18e The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a026e78 to
5d47b27
Compare
|
A wrangler prerelease is available for testing. You can install this latest build in your project with: npm install --save-dev https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-wrangler-4795You can reference the automatically updated head of this PR with: npm install --save-dev https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/prs/4795/npm-package-wrangler-4795Or you can use npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-wrangler-4795 dev path/to/script.jsAdditional artifacts:npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-create-cloudflare-4795 --no-auto-updatenpm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-kv-asset-handler-4795npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-miniflare-4795npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-pages-shared-4795npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-vitest-pool-workers-4795Note that these links will no longer work once the GitHub Actions artifact expires.
Please ensure constraints are pinned, and |
5d47b27 to
1038bee
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #4795 +/- ##
==========================================
+ Coverage 70.33% 70.39% +0.06%
==========================================
Files 297 297
Lines 15458 15458
Branches 3966 3966
==========================================
+ Hits 10872 10882 +10
+ Misses 4586 4576 -10 |
There was a problem hiding this comment.
When developing this, my IDE was configured to use Prettier 3 rather than the monorepo's Prettier 2. I'll need to reformat this code using the monorepo's version/config before landing. To make it easier to rebase this PR though, I've temporarily excluded vitest-pool-workers, so we don't end up with a bunch of conflicts from fixups.
1038bee to
98dd95e
Compare
| import type { WorkersProjectOptions } from "../pool/config"; | ||
| import type { Awaitable, inject } from "vitest"; | ||
|
|
||
| // Vitest will call `structuredClone()` to verify data is serialisable. |
There was a problem hiding this comment.
Ideally, we'd just drop support for Node 16, but our CI currently runs all tests with that. We could split the Vitest pool tests into a separate job maybe?
There was a problem hiding this comment.
I would be up for splitting the CI tests out and running in a more modern Node.js to avoid this polyfill.
|
In the "Maintainer Setup Instructions" I see:
But from what I can tell running |
|
I note that for this step:
If you run that command in the root of the monorepo you will get
|
petebacondarwin
left a comment
There was a problem hiding this comment.
OK I have read through all the vitest-pool-workers files (except for the example tests) and I haven't yet looked at the miniflare changes.
But thought it was worth dumping this first set of comments.
Nothing serious as far as I can tell so far.
I love the use of assertions throughout.
| import type { WorkersProjectOptions } from "../pool/config"; | ||
| import type { Awaitable, inject } from "vitest"; | ||
|
|
||
| // Vitest will call `structuredClone()` to verify data is serialisable. |
There was a problem hiding this comment.
I would be up for splitting the CI tests out and running in a more modern Node.js to avoid this polyfill.
| // Zod validators for types in runtime/config/workerd.ts. | ||
| // All options should be optional except where specifically stated. | ||
| // TODO: autogenerate these with runtime/config/workerd.ts from capnp | ||
| // TODO: autogenerate these with runtime/config/workerd.ts from cap |
| } | ||
| } | ||
|
|
||
| export default function (ctx: Vitest): ProcessPool { |
There was a problem hiding this comment.
As far as I can tell from the docs, this function could potentially be called multiple times if the server configuration changes (https://vitest.dev/advanced/pool.html#api). While I don't know how often that's likely to happen, it feels like it would be safer to initialise the spec cache here rather than at the top level, to ensure it's cleared on re-calls?
There was a problem hiding this comment.
Fixed in ba86a74. It looks like Vitest will call this function on re-runs too, in which case we do want to reuse instances. It looks like close() is only called when config changes/exiting though, not on re-runs, so updated that method to reset the instances.
| workersProject.testFiles.add(testFile); | ||
|
|
||
| parsedProjectOptions.add(project); | ||
| } |
There was a problem hiding this comment.
I'm not entirely sure what this block of code is doing. From the comments, it seems like there's a cache for projects across re-runs, but I'm not sure how parsedProjectOptions plays into that. Is it a cache for project options per run, in case a project is specified in multiple specs? What's the purpose of a top-level cache if project, options, and relativePath are re-generated per run?
There was a problem hiding this comment.
This makes sure...
allProjectscontains allworkersProjects- Each
workersProjectworkers project has up-to-date information for each call torunTests - We only parse project options once per project (this is what
parsedProjectOptionsis for). Notespecsmay contain the same project multiple times (e.g.[[PROJECT_1, "a.spec.ts"], [PROJECT_1, "b.spec.ts"], [PROJECT_2, "c.spec.ts"], ...]).
parsedProjectOptions isn't used beyond this for loop. Note also workersProjects are of type Project which includes an optional mf property for Miniflare instances that can be reused between runTests calls.
| const filesByProject = groupBy( | ||
| specs, | ||
| ([project]) => project, | ||
| ([, file]) => file | ||
| ); |
There was a problem hiding this comment.
Nit, but this may benefit clarity wise from just being a for loop:
| const filesByProject = groupBy( | |
| specs, | |
| ([project]) => project, | |
| ([, file]) => file | |
| ); | |
| const filesByProject = new Map() | |
| for ([project, file] of specs) { | |
| let projectEntry = filesByProject.get(project) ?? [] | |
| projectEntry.push(file) | |
| filesByProject.set(project, projectEntry) | |
| } |
There was a problem hiding this comment.
Should we error if a user has provided threads.isolate = true?
There was a problem hiding this comment.
No, we're setting this here because we're using the same node:worker_threads worker script from the threads pool, and we want to disable its isolation behaviour. We're not using the threads pool, it's just the script is meant for it, so reads its config from poolOptions.threads.
Users can use multiple pools in the same project (see https://vitest.dev/config/#poolmatchglobs-0-29-4), and we don't want to prohibit them from using options of other pools.
2f43dfc to
27bfdb5
Compare
Yep, this is outdated. I've updated the description to omit this. |
c925488 to
8a1ea9e
Compare
|
I think I've now fixed up all your comments so far @petebacondarwin and @penalosa, except:
|
Vitest will dispose of the pool and recreate it when the Vitest config changes. This was causing `Miniflare` instances to be disposed, then reused when the pool was recreated. This change moves the project cache from the module-level to a "pool-level" local variable.
Our mock agent implementation depends on `undici` internals. This change ensures we pin our version, so we don't accidentally upgrade to an incompatible version.
…ices Switch to passing `Miniflare` instance as parameter instead of `this`
Rename abort-all helper worker
Update not-implemented messages
Adds a `listDurableObjectIds()` test helper that behaves similarly to `getMiniflareDurableObjectIds()` from Miniflare 2's test environments.
Importantly, this change makes the Zod output type of `hyperdrives` assignable to the input type. This means we can pass the output of parsing options schemas back to the `new Miniflare()` constructor. Also adds a few more `hyperdrives` tests to verify this doesn't change behaviour.
Exposing `kCurrentWorker` to users for integration testing wasn't very nice. This is a common use case so this change ensures it's always bound and adds it directly to the `cloudflare:test` module.
This function aims to allow users to wait on side-effecting `waitUntil`s, then assert on their effects. `workerd` ignores the resolved value of `Promise`s, so exposing these may make it trickier to change the implementation of this function in the future.
`durableObjectBindingDesignators` includes the same and more information, so there's no need to pass this.
This makes it explicit which flags we're enabling, and helps avoid behaviour differences when running `wrangler dev` or deployed code.
Clarify `StackedStorageState` is per `Miniflare`-instance
Update docs in `config.ts`
49b9299 to
d8be18e
Compare

Closes all tickets in RM-17030 and RM-17070. Ref #4490.
What this PR solves / how to test:
This PR reintroduces Miniflare 2's Vitest unit testing environment to Miniflare 3! 🎉
Miniflare 3 runs your code inside
workerd, rather than Node. This means we can no longer access Workers runtime APIs directly inside Node, and instead need to run tests inside aworkerdprocess.We do this by implementing a Vitest Custom Pool, rather than a Test Environment. Pools can completely customise how test are run, whereas environments can only customise the global scope. Many thanks to @sheremet-va for implementing custom pools, and their continued support throughout this project.
Our pool starts
Miniflareinstances running the regular Vitest worker script, and uses WebSockets rather than inter-thread/process communication for message passing.This worker script expects to run inside a Node-like environment. To support this, we've implemented a "module fallback" service that provides Node-like disk-backed module resolution for
require()s andimportstatements inworkerd. Many thanks to @jasnell for implementing the fallback service API and unsafe-eval bindings this project also depends on.Right now, the pool only supports
vitest@1.1.3. I'll upgrade to the latest Vitest version soon.Features
🟢 Implemented, 🟡 Planned, 🔴 Not Planned
fetch(),scheduled()andqueue()exported handlers directlywaitUntil()edPromises have settledfetch()requestsenvironments andrunners insideworkerdChanges from
vitest-environment-miniflarevitest@1.1.3is currently supported@cloudflare/vitest-pool-workersenvironment: "miniflare"is nowpool: "@cloudflare/vitest-pool-workers"in Vitest configenvironmentOptions: { ... }is nowpoolOptions: { workers: { miniflare: { ... } } }in Vitest configwrangler.tomlby defaultgetMiniflareBindings()is nowimport { env } from "cloudflare:test"getMiniflareDurableObjectStorage()is nowimport { runInDurableObject } from "cloudflare:test", thenrunInDurableObject(stub, (instance, state) => { doSomethingWith(state.storage); })getMiniflareDurableObjectState()is nowimport { runInDurableObject } from "cloudflare:test", thenrunInDurableObject(stub, (instance, state) => { doSomethingWith(state); })getMiniflareDurableObjectInstance()is nowimport { runInDurableObject } from "cloudflare:test", thenrunInDurableObject(stub, (instance) => { doSomethingWith(instance); })runWithMiniflareDurableObjectGates(doSomething)is nowimport { runInDurableObject } from "cloudflare:test", thenrunInDurableObject(stub, doSomething)getMiniflareFetchMock()is nowimport { fetchMock } from "cloudflare:test"new ExecutionContext()is nowimport { createExecutionContext } from "cloudflare:test"getMiniflareWaitUntil()is nowimport { getWaitUntil } from "cloudflare:test"flushMiniflareDurableObjectAlarms()is nowimport { runDurableObjectAlarm } from "cloudflare:test", thenrunDurableObjectAlarm(stub)getMiniflareDurableObjectIds()is nowimport { listDurableObjectIds } from "cloudflare:test"Basic Config API Docs
Docs
Basic
cloudflare:testAPI DocsDocs
env: CloudflareTestEnv2nd argument passed to modules-format exported handlers. Contains bindings configured in top-level
miniflarepool options.fetchMock: import("undici").MockAgentDeclarative interface for mocking outbound
fetch()requests. Deactivated by default and reset before running each test file. Only mocksfetch()requests for the current test runner worker. Auxiliary workers should mockfetch()es with the regularfetchMock/outboundServiceoptions.runInDurableObject<O extends DurableObject, R>(stub: DurableObjectStub, callback: (instance: O, state: DurableObjectState) => R | Promise<R>): Promise<R>Runs
callbackinside the Durable Object pointed-to bystub's context. Conceptually, this temporarily replaces your Durable Object'sfetch()handler withcallback, then sends a request to it, returning the result. This can be used to call/spy-on Durable Object instance methods or seed/get persisted data.runDurableObjectAlarm(stub: DurableObjectStub): Promise<boolean>Immediately runs and removes the Durable Object pointed-to by
stub's alarm if one is scheduled. Returnstrueif an alarm ran, andfalseotherwise.listDurableObjectIds(namespace: DurableObjectNamespace): Promise<DurableObjectIds>Gets the IDs of all objects that have been created in the
namespace. RespectsisolatedStorageif enabled, i.e. objects created in a different test won't be returned.createExecutionContext(): ExecutionContextCreates an instance of
ExecutionContextfor use as the 3rd argument to modules-format exported handlers.getWaitUntil<T extends unknown[]>(ctx: ExecutionContext): Promise<T>Waits for all
ExecutionContext#waitUntil()edPromises to settle. Only accepts instances ofExecutionContextreturned bycreateExecutionContext().createScheduledController(options?: FetcherScheduledOptions): ScheduledControllerCreates an instance of
ScheduledControllerfor use as the 1st argument to modules-formatscheduled()exported handlers.getScheduledResult(ctrl: ScheduledController, ctx: ExecutionContext): Promise<FetcherScheduledResult>Gets the "no retry" state of the
ScheduledController, and waits for allExecutionContext#waitUntil()edPromises to settle. Only accepts instances ofScheduledControllerreturned bycreateScheduledController(), and instances ofExecutionContextreturned bycreateExecutionContext().createMessageBatch(queueName: string, messages: ServiceBindingQueueMessage[]): MessageBatchCreates an instance of
MessageBatchfor use as the 1st argument to modules-formatqueue()exported handlers.getQueueResult(batch: MessageBatch, ctx: ExecutionContext): Promise<FetcherQueueResult>Gets the ack/retry state of messages in the
MessageBatch, and waits for allExecutionContext#waitUntil()edPromises to settle. Only accepts instances ofMessageBatchreturned bycreateMessageBatch(), and instances ofExecutionContextreturned bycreateExecutionContext().Miniflare Changes
This PR includes a bunch of changes in Miniflare to support the custom Vitest pool.
These changes are contained within commits starting with
[miniflare].User facing changes to non
unsafe*options/methods have changesets.fetch()esformatZodError()methodthistoMiniflareinstance in function-valued custom servicesz.input()for user facing options typeskCurrentWorkersymbolrootPathsDurableObjectStubdetection when Durable Object JS RPC is enabledMaintainer Setup Instructions
pnpm@8(lower versions don't correctly handle thevitestpeer-dependency requirement)pnpm installin the monorepo rootpnpm buildin the monorepo rootpnpm exec vitest --versioninpackages/vitest-pool-workersand verify this outputsvitest/1.1.3pnpm testinpackages/vitest-pool-workerspackages/vitest-pool-workers/testPre-Release Setup Instructions
Clone https://github.com/mrbbot/vitest-pool-workers-prerelease-getting-started and follow the instructions in the README.
Reviewing Guidance
This is a pretty chunky PR. I've done my best to keep individual features to separate commits, but some later commits are fixups to earlier ones. Most commits have descriptions explaining what they're adding/changing. Very happy to walkthrough how everything fits together if that would be helpful too, but as an outline:
packages/vitest-pool-workerssrcconfig: entrypoint for the@cloudflare/vitest-pool-workers/configsub-exportmock-agent: entrypoint forcloudflare:mock-agentworker library, subset ofundicirequired to supportMockAgentpool: code running inside Node.jsconfig.ts: schemas forpoolOptions.workersindex.ts: entrypoint for the@cloudflare/vitest-pool-workerspackageloopback.ts: custom service for implementing snapshot storage/stacked storagemodule-fallback.ts: fallback service implementation providing Node-likeimport/requiremodule resolutionworker: code running insideworkerdlib: polyfills for modules unsupported byworkerd, files in this directory map directly to imports insideworkerd(i.e.lib/cloudflare/test.tsbecomescloudflare:test)durable-objects.ts: Durable Object test helpers (runInDurableObject(),runDurableObjectAlarm())env.ts: environment test helpers (env)events.ts: exported handler test helpers (createExecutionContext(),getWaitUntil(),createScheduledController(),getScheduledResult(),createMessageBatch(),getQueueResult())fetch-mock.ts: fetch mocking test helpers (fetchMock)import.ts: code for importing arbitrary modules in a Worker using Vite's pipeline, mainly used for importing the configuredmainmoduleindex.ts: entrypoint for the Worker that gets executed insideworkerdto run tests, contains the Durable Object that actually imports the Vitest runner to run teststestbasic: tests built-in Vitest functionalitykv: tests basic functionality of all featuresstacked: stress test for isolated storagecloudflare-test.d.ts: user facingcloudflare:testtypes, should probably be generated automaticallyRight now, we have a few example tests in
packages/vitest-pool-workers/test. I'm planning to add a wider suite of tests here soon, in addition to some "meta-tests" that verify things like snapshots work correctly (tests fail when snapshot is incorrect, snapshots can be updated, etc.).Changesets have only been included for the
miniflarepackage. We're not quite ready to release@cloudflare/vitest-pool-workersyet, so it's been markedprivate: true, and won't be included in the regular release process.Author has addressed the following:
@cloudflare/vitest-pool-workersyet)@cloudflare/vitest-pool-workersyet, upcoming meeting with PCX to discussNote for PR author:
We want to celebrate and highlight awesome PR review! If you think this PR received a particularly high-caliber review, please assign it the label
highlight pr reviewso future reviewers can take inspiration and learn from it.