feat: add official test utils for remote functions - #15671
Conversation
Adds official test utilities for unit testing remote functions without mocking SvelteKit internals. - createTestEvent(options) builds a mock RequestEvent with sensible defaults - withRequestContext(event, fn) establishes the request store context using with_request_store - callRemote(fn, arg, options) auto-detects function type (query/command/form) and sets the appropriate HTTP method - setLocals(locals) modifies the current test's request context - HttpValidationError extends HttpError to surface Standard Schema validation issues for test assertions - svelteKitTest() Vitest plugin resolves virtual modules, transforms .remote.ts files, and injects auto-context per test via als.enterWith()
Introduces component testing: mockRemote(fn) controls what data
components receive when rendering remote functions, without executing
server logic.
The svelteKitTest plugin gains a mode option. In component mode, it
redirects .remote.ts imports to virtual module IDs via resolveId + load,
bypassing the production sveltekit() plugin's transform. The load hook
reads the original source, parses exports via regex, and generates
client stubs pointing to a mock runtime.
The mock runtime provides MockQueryProxy with $state-backed reactive
properties, mock commands with .pending tracking, and mock forms with
a recursive Proxy for nested field access.
mockRemote API is chainable:
mockRemote(fn).returns(data)
mockRemote(fn).withFieldValues({ email: 'alice@example.com' })
mockRemote(fn).withFieldIssues({ name: [{ message: 'Required' }] })
Covers server-side testing (auto-context, setLocals, callRemote, withRequestContext, validation errors) and component testing (mockRemote with queries, commands, and forms). Includes Svelte component + test file pairs showing the full pattern. Covers dual-mode Vitest project configuration for projects that need both server and component tests.
🦋 Changeset detectedLatest commit: 020d817 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
@spences10 would love to get your perspective here, since you've put a lot of good work into evangelizing svelte testing patterns with sveltest |
|
Thanks for the mention @brendan-morin! If this lands, I'd love to update sveltest to use these utilities and document the patterns around them. |
|
|
||
| // Component rendering tests fail on Node 18 with lifecycle_function_unavailable | ||
| // "mount(...) is not available on the server" | ||
| describe.skip('form component rendering', () => { |
There was a problem hiding this comment.
I'm not sure what the recommended process for this handling this, but basically this test fails the node 18 checks in CI but passes everything else. As far as I can tell it's related to trying to do rendering in this test.
Having skipped tests here isn't a long term goal, but is there anything about node 18 or its deps that is special that we'd expect it to fail here where the other versions don't have problems (maybe a vitest-browser-svelte compatibility issue)?
|
@brendan-morin this really would be SUPER useful. I know this PR currently has conflicts but I wanted to give a +1 on this functionality. I love the remote functions and async svelte capabilities but we are having to use the pattern of injecting remotes into components in order to be able to unit test them which is adding a lot of technical debt we will need to refactor once we have an official / better way of doing it so the quicker we can get something like this the better. |
|
Hi Brendan! Just helping you out because this might be clutter for maintainers, so I don't expect them to respond any time soon. This targets main. Feature work goes to version-3 now. So this needs a rebase before anything else. Remote functions are still changing. #16330 is an open breaking change to how redirects work, today. This PR freezes that moving surface into two public export paths. Every future change to remote functions then also breaks Now for my opinion: Totally unneeded feature, and I'm surprised this is something people are liking. The wrapper around a query, command or form is framework code with a fixed set of outcomes: a result, a validation failure, an error or redirect. That behavior is deterministic (In theory). Calling your function through it in a unit test just re-tests the framework. Although it might sound ancient, this is an MVC lesson: the controller stays thin and the logic lives somewhere testable. If a remote function is hard to test in this day and age of development, the logic is in the wrong place. Move it into a plain function and it tests with zero tooling. The remote function then just validates input and calls it, and there is nothing left worth testing through the framework. |
|
@Nic-Polumeyv I appreciate the practical advice in your comment. Would love to have submitted an RFC but svelte's RFC repo is clearly no longer in use. Happy to update, rebase, and address nits pending feedback from maintainers on the concept itself.
Perhaps reflect on this a bit then before passing a judgement on value. It is true there is nothing in this PR that a capable and motivated user could not do on their own, but I would disagree with you that just because it's possible does not mean that providing utilities does not provide value. After all, there is essentially nothing svelte/kit can do that react/next can't, yet it exists and is used because most of us agree it provides a better experience. |
|
I didn't say this lacks value on the grounds that users could build it themselves. I used the word "unneeded," and to call something unneeded is in fact an acknowledgment of its value - value that, in my experience and in this particular case, I don't consider necessary. createTestState hardcodes its own handleValidationError with no mechanism for invoking the app's hook, so any app that customizes validation errors is testing against behavior it does not actually have. The documentation likewise teaches asserting on e.issues, which never exists at runtime, since issues never leaves the server. And the Kit/Next comparison responds to an argument I never advanced. I recognize that my position may be frustrating, but my intent in spending time analyzing this PR was to help you strengthen it - and, ideally, to have my objection proven wrong on the merits - rather than to have it treated as a flawed perspective on my part. |
|
Thanks for the detailed comment and responding with your thoughts. I think the major reason, at least for us, that people are liking / wanting something like this is because at the moment there is no official guidance or examples on unit testing remotes - for us that is especially true when it comes to component testing. While playwright is a great solution for E2E, it doesn't solve the unit testing problem. I think on the server side, it is a clear boundary and a good process we are using is to create functionality in standard However this all starts to fall apart as I mentioned in the UI components which effectively mix server and client and are not just logic, we need / want to test the expected UI. The really great benefit of the new remotes for us is the eradication of prop drilling and needing to pass the data down from the page server actions into components as props when that component can and should be responsible for their own data management / state. In todays tooling - we are using vitest with jsdom and
For us the only real solution from the above is option 1 and creating wrapper components just to be able to unit test the remote enabled sibling. But like I say I think this is a really ugly hack and still requires the ugly mocks. For us, the distinct difference in having two components on the server - i.e. A potential solution I have wondered about - and it could be a stupid one - as it would also involve an extra dependancy outside of svelte - would be to use MSW or similar to intercept the fetch calls. That way we could intercept the remote request and simply return the shapes we are wanting to test against without having to think about remote internals. I don't think we currently have a way to get these generated url's though. If we could do something like this maybe it could work but again - this could be a wholly incorrect and silly idea. import { http, HttpResponse } from 'msw';
import {someRemote} from ('some.remote');
export const handlers = [
http.get(someRemote.remoteUrl, () => {
return HttpResponse.json({
id: 'abc-123',
firstName: 'John',
lastName: 'Maverick'
});
}),
] |
closes #14796
closes #14249
Summary
Add official test utilities for unit testing remote functions and components that use them. The target here is a lightweight and intuitive DX for general remote function testing that is sufficiently flexible. This has the added benefit for being groundwork for additional testing utilities as SvelteKit continue to grow.
Quick Examples
The easiest way to understand the intent of this PR is to consider the following examples (the included docs also are a great place to start):
Unit Testing Remote Functions
Current State
Naive testing of remote functions fails:
The primary workaround is mocking complicated $app/server internals:
After this PR
When using the svelteKitTest plugin, testing remote functions "just works"
Testing Components with Remote Functions
Current State
Given a component that uses a remote form:
There is no standard way to unit test this component. The best pattern I've seen for this is the functional core/imperative shell pattern, which still requires manually mocking internal RF functionality. Or jumping into full end-to-end testing via e.g. Playwright.
After this PR
We can now easily mock state for remote functions for use in component tests via a standard interface:
What's Included
@sveltejs/kit/testexports:createTestEvent(options)— mock RequestEvent with sensible defaultswithRequestContext(event, fn)— establish request store context for a callbackcallRemote(fn, arg, options)— convenience wrapper with typed overloads, auto-detects GET/POST from function typesetLocals(locals)— modify event.locals on the current test contextmockRemote(fn)— chainable builder: .returns(), .throws(), .resolves(), .withFieldValues(), .withFieldIssues()HttpValidationError— HttpError subclass with typed .issues for schema validation assertionscreateTestState(options)— shared RequestState construction@sveltejs/kit/test/vitestexports:svelteKitTest(options?)— Vitest plugin with two modes:als.enterWith()Documentation to demonstrate basic usage
Key Design Decisions
I made a judgement call on a few things here, but it's entirely possible there are more idiomatic ways to go about this, so I'm open to any feedback on these.
Auto-context uses
als.enterWith(), notsync_store. Settingsync_storedirectly doesn't survive nestedwith_request_storecalls (the finally block resets it).enterWithsets a persistent ALS context that survives becauseAsyncLocalStoragemaintains a context stack. The dev server uses the same mechanism, which was the inspiration here.__test_set_request_storeand__test_clear_request_storeare exported from@sveltejs/kit/internal/server. Thealsinstance inevent.jsis module-private — there's no way to callenterWithon it without exporting a function. These are technically part of the public API (event.js), but my hope was the__test_prefix signals test infrastructure use. This was purely additive, no modifications to existing code.handleValidationErrorthrowsHttpValidationErrordirectly. In production,handleValidationErrorreturns{ message: 'Bad Request' }and issues are only logged to console. In tests, our handler throwsHttpValidationError(which extendsHttpError), short-circuiting the framework'serror(400, ...)call. Because this is a test util, the goal was to give test consumers easy typed access to.issuesfor any assertions.Component mode uses virtual module redirect to coexist with
sveltekit(). The production plugin checks file paths for.remote.ts—enforce: 'pre'alone doesn't prevent it from also transforming the file. OurresolveIdhook redirects.remote.tsimports to virtual IDs (\0sveltekit-test-mock:{hash}) that don't match the production plugin's pattern.Test plan
This should be fairly comprehensively tested. I'm a believer in test-as-documentation as well, so if any tests are unclear please let me know.
pnpm run format/pnpm run lint/pnpm run check/pnpm -F @sveltejs/kit test:unit@sveltejs/kit/testand@sveltejs/kit/test/vitestOrganization
I tried splitting this into a couple commits (server testing, component testing, docs) to hopefully make it easier to review. I'm happy to break this down further as needed.
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
Tests
pnpm testand lint the project withpnpm lintandpnpm checkChangesets
pnpm changesetand following the prompts. Changesets that add features should beminorand those that fix bugs should bepatch. Please prefix changeset messages withfeat:,fix:, orchore:.Edits