Skip to content

feat: add official test utils for remote functions - #15671

Open
brendan-morin wants to merge 9 commits into
sveltejs:mainfrom
brendan-morin:test-utils-pr
Open

feat: add official test utils for remote functions#15671
brendan-morin wants to merge 9 commits into
sveltejs:mainfrom
brendan-morin:test-utils-pr

Conversation

@brendan-morin

@brendan-morin brendan-morin commented Apr 7, 2026

Copy link
Copy Markdown

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:

import { getUser } from './data.remote.ts';

test('returns user data', async () => {
  // Attempting to call remote function throws: "Could not get the request store..."
  const result = await getUser('user-123');
});

The primary workaround is mocking complicated $app/server internals:

const mockEvent = {
  url: new URL('http://localhost'),
  request: new Request('http://localhost'),
  locals: { user: { id: '123' } },
  cookies: { get: () => undefined, getAll: () => [], set: () => {}, delete: () => {}, serialize: () => '' },
  fetch: globalThis.fetch,
  getClientAddress: () => '127.0.0.1',
  params: {},
  route: { id: '/' },
  setHeaders: () => {},
  isDataRequest: false,
  isSubRequest: false,
  isRemoteRequest: false
};

vi.mock('$app/server', async (importOriginal) => {
  const actual = await importOriginal();
  return {
    ...actual,
    getRequestEvent: vi.fn(() => mockEvent),
    query: (schemaOrHandler, arg2) => {
      let handler = arg2 ?? schemaOrHandler;
      handler.__ = { type: 'query' }; // relies on undocumented internal metadata
      return handler;
    }
  };
});

test('returns user data', async () => {
  // "works" but getUser is now the raw handler, not a real remote function.
  // schema validation, request context, and caching are all bypassed.
  const result = await getUser('user-123');
});

After this PR

When using the svelteKitTest plugin, testing remote functions "just works"

import { getUser } from './data.remote.ts';

test('returns user data', async () => {
  // the svelteKitTest() plugin establishes a request context per test,
  // so remote functions execute with their real validation and context
  const result = await getUser('user-123');
  expect(result).toEqual({ name: 'Alice' });
});

Testing Components with Remote Functions

Current State

Given a component that uses a remote form:

<!-- src/lib/components/contactUsForm.svelte -->
<script>
  import { contactForm } from './contact.remote.ts';
</script>

<form {...contactForm}>
  <input {...contactForm.fields.name.as('text')} />

  {#if contactForm.fields.name.issues()}
    {#each contactForm.fields.name.issues() as issue}
      <span class="error">{issue.message}</span>
    {/each}
  {/if}

  {#if contactForm.result}
    <p class="success">Message sent!</p>
  {/if}

  <button>Send</button>
</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:

import { mockRemote } from '@sveltejs/kit/test';
import { contactForm } from './contact.remote.ts';

test('shows validation errors on the name field', async () => {
  mockRemote(contactForm).withFieldIssues({
    name: [{ message: 'Name is required' }]
  });

  // render component -- contactForm.fields.name.issues() returns the mock,
  // so the component renders <span class="error">Name is required</span>
});

test('pre-populates the name field', async () => {
  mockRemote(contactForm).withFieldValues({
    name: 'Alice'
  });

  // render component -- contactForm.fields.name.as('text') returns
  // input props with value 'Alice'
});

test('shows success message after submission', async () => {
  mockRemote(contactForm).returns({ sent: true });

  // render component -- contactForm.result is { sent: true },
  // so the component renders <p class="success">Message sent!</p>
});

What's Included

  • @sveltejs/kit/test exports:

    • createTestEvent(options) — mock RequestEvent with sensible defaults
    • withRequestContext(event, fn) — establish request store context for a callback
    • callRemote(fn, arg, options) — convenience wrapper with typed overloads, auto-detects GET/POST from function type
    • setLocals(locals) — modify event.locals on the current test context
    • mockRemote(fn) — chainable builder: .returns(), .throws(), .resolves(), .withFieldValues(), .withFieldIssues()
    • HttpValidationError — HttpError subclass with typed .issues for schema validation assertions
    • createTestState(options) — shared RequestState construction
  • @sveltejs/kit/test/vitest exports:

    • svelteKitTest(options?) — Vitest plugin with two modes:
      • Server mode: resolves virtual modules, transforms .remote.ts files, injects auto-context per test via als.enterWith()
      • Component mode: redirects .remote.ts imports to a mock runtime with reactive $state-backed objects
  • 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(), not sync_store. Setting sync_store directly doesn't survive nested with_request_store calls (the finally block resets it). enterWith sets a persistent ALS context that survives because AsyncLocalStorage maintains a context stack. The dev server uses the same mechanism, which was the inspiration here.

  • __test_set_request_store and __test_clear_request_store are exported from @sveltejs/kit/internal/server. The als instance in event.js is module-private — there's no way to call enterWith on 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.

  • handleValidationError throws HttpValidationError directly. In production, handleValidationError returns { message: 'Bad Request' } and issues are only logged to console. In tests, our handler throws HttpValidationError (which extends HttpError), short-circuiting the framework's error(400, ...) call. Because this is a test util, the goal was to give test consumers easy typed access to .issues for any assertions.

  • Component mode uses virtual module redirect to coexist with sveltekit(). The production plugin checks file paths for .remote.tsenforce: 'pre' alone doesn't prevent it from also transforming the file. Our resolveId hook redirects .remote.ts imports 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.

  • Unit tests (server-side): createTestEvent, withRequestContext, callRemote, setLocals, auto-context, ALS stacking, HttpValidationError, schema validation
  • Type tests: overload inference for query/command/form, arg type enforcement
  • Unit tests component mode transform (resolveId + load pipeline), mockRemote error path
  • End-to-end: mock query/command/form data, reactive interface transitions, form component rendering with vitest-browser-svelte + Chromium
  • pnpm run format / pnpm run lint / pnpm run check / pnpm -F @sveltejs/kit test:unit
  • Verified generated types include both @sveltejs/kit/test and @sveltejs/kit/test/vitest

Organization

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:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

Brendan Morin added 4 commits April 7, 2026 13:21
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-bot

changeset-bot Bot commented Apr 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 020d817

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Minor

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

@svelte-docs-bot

Copy link
Copy Markdown

@brendan-morin

Copy link
Copy Markdown
Author

@spences10 would love to get your perspective here, since you've put a lot of good work into evangelizing svelte testing patterns with sveltest

@spences10

Copy link
Copy Markdown
Member

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', () => {

@brendan-morin brendan-morin Apr 9, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

@kevpye-fabdata

Copy link
Copy Markdown

@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.

@Nic-Polumeyv

Copy link
Copy Markdown
Contributor

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 @sveltejs/kit/test. The component mock runtime is worse. It's a second implementation of the remote function objects, and it has to stay bug for bug faithful to the real $state-backed ones, forever. The utils also don't do what the description says. The pitch is real validation and context, not mocks. But handleValidationError here throws HttpValidationError, while production returns a generic 400 and logs. Tests pass against semantics that don't exist in prod. That's the exact failure mode this PR says hand-rolled mocks cause. Kit core has never depended on a test runner. This adds a vitest plugin to @sveltejs/kit and ties core releases to vitest's browser mode API, which is itself still moving. That tooling lives in the ecosystem today (sveltest, vitest-browser-svelte). mockRemote belongs there too. .returns() / .throws() is a mocking DSL vi.mock already covers. A defect underneath is the request store error from #13899. That's a bug with a targeted fix (#16596 covers the sequence case in #14249), not a reason for new API. Handler logic already unit tests fine as plain functions, the functional core pattern from #14796. The only genuinely unsolved surface is component mocking, and that's ecosystem work. 2.5k lines of new public API without an RFC is the wrong order.

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.

@brendan-morin

brendan-morin commented Aug 2, 2026

Copy link
Copy Markdown
Author

@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.

I'm surprised this is something people are liking

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.

@Nic-Polumeyv

Copy link
Copy Markdown
Contributor

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.

@kevpye-fabdata

Copy link
Copy Markdown

Hi @Nic-Polumeyv

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 *.server.ts files and then have an accompanying *.remote.ts that is a thin wrapper to be able to use the code. This allows us to unit test the server code without worrying about the remote semantics too much. The only thing we care about testing at that point is any auth wrappers etc that we apply to the remotes. This allows us to use remotes when desired and the same code in standard server routes where using the remote is not necessary.

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 @testing-library/svelte/vite. If we have a component that imports a remote using the standard syntax described in the svelte docs, we simply cannot unit test it with this setup without the workarounds being discussed in this thread and described in the svelte testing site. That feels like a major flaw to people working with remotes. The solution we have at the moment is the following and it is not very nice in my opinion given the elegance of working with remotes before you introduce the need to test.

  1. We have to create a component that accepts remotes as props typed using the ReturnTypesyntax from ts.
  • This is already ugly and not how the documentation shows in examples
  • This then allows us to construct the mocks in our unit tests - but as mentioned we are then having to know the remote implementation from svelte and keep it in line with changes.
  1. We now have 2 choices,
  • we create a sister wrapper component that actually imports the remotes. All this component does is pass the remotes to the main component along with any other additional props This is the component that actually gets used by the application code, the main component is never directly imported. This wrapper component must be excluded from unit testing - as, like discussed, it cannot be imported into a test file because the test file will fail before running any tests.
  • We don't have the wrapper and then we have to go back to passing the remotes into the original component from any page that uses it - precisely the situation we don't want as we have to import the remotes on every route that uses the component.

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. server.ts + remote.ts vs the two UI components importing remotes is that the dual file server pattern actually serves a purpose - i.e. code reuse between server only endpoints like API routes and component use via remotes. We feel we should not have to declare two UI components just to enable testing - that feels like a smell.

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'
		});
	}),
]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unit tests of remote functions Could not get the request store During Server Test

5 participants