-
Notifications
You must be signed in to change notification settings - Fork 742
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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-4795 You 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-4795 Or you can use npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-wrangler-4795 dev path/to/script.js Additional artifacts:npx https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-create-cloudflare-4795 --no-auto-update npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-kv-asset-handler-4795 npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-miniflare-4795 npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-pages-shared-4795 npm install https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/7933607154/npm-package-cloudflare-vitest-pool-workers-4795 Note 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 |
.prettierignore
Outdated
@@ -37,3 +37,4 @@ fixtures/**/dist/** | |||
|
|||
# Miniflare shouldn't be formatted with the root `prettier` version | |||
packages/miniflare | |||
packages/vitest-pool-workers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would be up for splitting the CI tests out and running in a more modern Node.js to avoid this polyfill.
@@ -9,7 +9,12 @@ import { zAwaitable } from "../../shared"; | |||
|
|||
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
capnp -> cap ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Undone in 709fd43
} | ||
} | ||
|
||
export default function (ctx: Vitest): ProcessPool { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sure...
allProjects
contains allworkersProject
s- Each
workersProject
workers project has up-to-date information for each call torunTests
- We only parse project options once per project (this is what
parsedProjectOptions
is for). Notespecs
may 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 workersProject
s 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated in 292cb60
// serialisable. `getSerializableConfig()` may also return references to | ||
// the same objects, so override it with a new object. | ||
config.poolOptions = { | ||
threads: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we error if a user has provided threads.isolate = true
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 aworkerd
process.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
Miniflare
instances 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 andimport
statements 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
[email protected]
. I'll upgrade to the latest Vitest version soon.Features
🟢 Implemented, 🟡 Planned, 🔴 Not Planned
fetch()
,scheduled()
andqueue()
exported handlers directlywaitUntil()
edPromise
s have settledfetch()
requestsenvironment
s andrunner
s insideworkerd
Changes from
vitest-environment-miniflare
[email protected]
is currently supported@cloudflare/vitest-pool-workers
environment: "miniflare"
is nowpool: "@cloudflare/vitest-pool-workers"
in Vitest configenvironmentOptions: { ... }
is nowpoolOptions: { workers: { miniflare: { ... } } }
in Vitest configwrangler.toml
by 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:test
API DocsDocs
env: CloudflareTestEnv
2nd argument passed to modules-format exported handlers. Contains bindings configured in top-level
miniflare
pool options.fetchMock: import("undici").MockAgent
Declarative 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
/outboundService
options.runInDurableObject<O extends DurableObject, R>(stub: DurableObjectStub, callback: (instance: O, state: DurableObjectState) => R | Promise<R>): Promise<R>
Runs
callback
inside 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. Returnstrue
if an alarm ran, andfalse
otherwise.listDurableObjectIds(namespace: DurableObjectNamespace): Promise<DurableObjectIds>
Gets the IDs of all objects that have been created in the
namespace
. RespectsisolatedStorage
if enabled, i.e. objects created in a different test won't be returned.createExecutionContext(): ExecutionContext
Creates an instance of
ExecutionContext
for use as the 3rd argument to modules-format exported handlers.getWaitUntil<T extends unknown[]>(ctx: ExecutionContext): Promise<T>
Waits for all
ExecutionContext#waitUntil()
edPromise
s to settle. Only accepts instances ofExecutionContext
returned bycreateExecutionContext()
.createScheduledController(options?: FetcherScheduledOptions): ScheduledController
Creates an instance of
ScheduledController
for 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()
edPromise
s to settle. Only accepts instances ofScheduledController
returned bycreateScheduledController()
, and instances ofExecutionContext
returned bycreateExecutionContext()
.createMessageBatch(queueName: string, messages: ServiceBindingQueueMessage[]): MessageBatch
Creates an instance of
MessageBatch
for 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()
edPromise
s to settle. Only accepts instances ofMessageBatch
returned bycreateMessageBatch()
, and instances ofExecutionContext
returned 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()
methodthis
toMiniflare
instance in function-valued custom servicesz.input()
for user facing options typeskCurrentWorker
symbolrootPath
sDurableObjectStub
detection when Durable Object JS RPC is enabledMaintainer Setup Instructions
pnpm@8
(lower versions don't correctly handle thevitest
peer-dependency requirement)pnpm install
in the monorepo rootpnpm build
in the monorepo rootpnpm exec vitest --version
inpackages/vitest-pool-workers
and verify this outputsvitest/1.1.3
pnpm test
inpackages/vitest-pool-workers
packages/vitest-pool-workers/test
Pre-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-workers
src
config
: entrypoint for the@cloudflare/vitest-pool-workers/config
sub-exportmock-agent
: entrypoint forcloudflare:mock-agent
worker library, subset ofundici
required to supportMockAgent
pool
: code running inside Node.jsconfig.ts
: schemas forpoolOptions.workers
index.ts
: entrypoint for the@cloudflare/vitest-pool-workers
packageloopback.ts
: custom service for implementing snapshot storage/stacked storagemodule-fallback.ts
: fallback service implementation providing Node-likeimport
/require
module resolutionworker
: code running insideworkerd
lib
: polyfills for modules unsupported byworkerd
, files in this directory map directly to imports insideworkerd
(i.e.lib/cloudflare/test.ts
becomescloudflare: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 configuredmain
moduleindex.ts
: entrypoint for the Worker that gets executed insideworkerd
to run tests, contains the Durable Object that actually imports the Vitest runner to run teststest
basic
: tests built-in Vitest functionalitykv
: tests basic functionality of all featuresstacked
: stress test for isolated storagecloudflare-test.d.ts
: user facingcloudflare:test
types, 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
miniflare
package. We're not quite ready to release@cloudflare/vitest-pool-workers
yet, so it's been markedprivate: true
, and won't be included in the regular release process.Author has addressed the following:
@cloudflare/vitest-pool-workers
yet)@cloudflare/vitest-pool-workers
yet, 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 review
so future reviewers can take inspiration and learn from it.