Skip to content

feat: add official test utils for remote functions#15671

Open
brendan-morin wants to merge 4 commits intosveltejs:mainfrom
brendan-morin:test-utils-pr
Open

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

Conversation

@brendan-morin
Copy link
Copy Markdown

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

closes #14796

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
Copy link
Copy Markdown

changeset-bot bot commented Apr 7, 2026

🦋 Changeset detected

Latest commit: 051f676

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

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

1 participant