Skip to content
qt

Documentation

Testing Guide

Unit-test mutations that call invalidateTags, component tests that react to SSE events, and widget tests that consume server status — all without a real server or SSE connection.

Overview

The @tanstack-tools/query-tags/testing entry exports four utilities that cover the three most common testing scenarios:

createMockInvalidationSystem

Server-side replacement

Drop-in mock for tagInvalidation. Captures every invalidateTags() call with assertion helpers.

createMockInvalidatorClient

Client-side replacement

Mock InvalidatorClient where every method is a vi.fn() spy with sensible defaults.

createTestProvider

React test wrapper

Creates a TagInvalidationProvider wrapper + queryClient + runtime for Testing Library render().

simulateInvalidation

SSE event simulation

Injects an invalidation message directly into the runtime — no SSE connection needed.

Import from /testing
# The /testing entry ships with the main package — no separate install needed.
# Peer deps: vitest (or any framework that exposes a compatible vi.fn)
import {
  createMockInvalidationSystem,
  createMockInvalidatorClient,
  createTestProvider,
  simulateInvalidation,
} from "@tanstack-tools/query-tags/testing"
The /testing entry relies on vi.fn from Vitest being available on globalThis.vi, or you can pass { vi } explicitly to createMockInvalidatorClient. It is safe to import in any Vitest test file.

Setup

The examples on this page share a common tag contract. Define it once and import it in both your application code and your test files.

tag-contract.ts — shared tag definitions
// src/lib/tag-contract.ts
import { defineTags } from "@tanstack-tools/query-tags";

export const appTags = defineTags({
  todos: {
    list:  () => ["todos"],
    byId:  (id: number) => ["todo", id],
  },
} as const);

Testing Server Mutations

Use createMockInvalidationSystem to assert that your server actions and mutations call invalidateTags with the correct tags. The mock implements the same interface as the real system — swap it in via dependency injection or a module-level variable.

1
The code under test
// src/server/todos.server.ts
import { createServerFn } from "@tanstack/react-start";
import { tagInvalidation } from "#/lib/invalidation-system";
import { appTags } from "#/lib/tag-contract";

export const createTodoServerFn = createServerFn({ method: "POST" })
  .handler(async ({ data }: { data: { name: string } }) => {
    await db.todos.create({ data: { name: data.name } });

    await tagInvalidation.invalidateTags({
      tags: [appTags.todos()],  // invalidates todos.list + todos.byId
    });
  });
2
Basic assertion
// src/server/todos.test.ts
import { describe, it } from "vitest";
import { createMockInvalidationSystem } from "@tanstack-tools/query-tags/testing";
import { appTags } from "#/lib/tag-contract";

describe("createTodo", () => {
  it("invalidates the todos tag group", async () => {
    const system = createMockInvalidationSystem();

    // Call the logic under test, passing the mock system in place of the real one
    await system.invalidateTags({ tags: [appTags.todos()] });

    // Assert every leaf under appTags.todos() was invalidated
    system.expectTagsInvalidated(['"todos"']);
  });
});
Tags are stored as their serialized form: "todos" for a string tag, ["todo",42] for an array tag. Use appTags.todos.list() on the query side — normalizeTag converts it to the same string the mock captures.
3
Advanced assertions
// Test helpers from createMockInvalidationSystem
const system = createMockInvalidationSystem();

// Run your mutation / server action
await runMyMutation(system);

// Assert specific tags were invalidated
system.expectTagsInvalidated(['"todos"', '["todo",42]']);

// Assert certain tags were NOT touched
system.expectTagsNotInvalidated(['"products"']);

// Count how many times a tag was invalidated
const count = system.getInvalidationCount('"todos"');

// Inspect the raw messages for custom assertions
console.log(system.emittedMessages);
// [{ scopeKey: "global", tags: ['"todos"', '["todo",42]'] }]

// Reset between test cases
system.reset();

Component Tests with simulateInvalidation

Use createTestProvider to wrap your component in a full TagInvalidationProvider, then call simulateInvalidation to inject a synthetic SSE event directly into the runtime. No server or WebSocket needed.

Component test — TodoList refetches on invalidation
// src/components/TodoList.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import {
  createTestProvider,
  simulateInvalidation,
} from "@tanstack-tools/query-tags/testing";
import { appTags } from "#/lib/tag-contract";
import { TodoList } from "./TodoList";

describe("TodoList", () => {
  it("refetches when the todos tag is invalidated", async () => {
    const { TestProvider, queryClient, runtime } = createTestProvider();

    // Seed the cache
    queryClient.setQueryData(["todos"], [{ id: 1, name: "Buy oat milk" }]);

    render(<TodoList />, { wrapper: TestProvider });

    expect(screen.getByText("Buy oat milk")).toBeInTheDocument();

    // Simulate an SSE invalidation event (no real server or SSE needed)
    await simulateInvalidation(runtime, { tags: [appTags.todos.list()] });

    // Query is now stale — a refetch has been scheduled
    await waitFor(() => {
      expect(queryClient.isFetching({ queryKey: ["todos"] })).toBe(1);
    });
  });
});
simulateInvalidation calls runtime.processMessage() directly, bypassing SSE. The runtime invalidates the matching queries in the provided queryClient exactly as it would in production.

Mocking the InvalidatorClient

Components that call the server control plane (pause, enable/disable tags, fetch status) use the InvalidatorClient interface. Use createMockInvalidatorClient to provide a fully-typed mock where every method is a vi.fn() spy.

Mocking status + asserting on the UI
// src/components/StatusWidget.test.tsx
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { vi } from "vitest";
import {
  createMockInvalidatorClient,
  createTestProvider,
} from "@tanstack-tools/query-tags/testing";
import { StatusWidget } from "./StatusWidget";

describe("StatusWidget", () => {
  it("renders connected client count from the server", async () => {
    const client = createMockInvalidatorClient({ vi });

    // Override the default resolved value for getStatus
    client.mocks.getStatus.mockResolvedValue({
      paused: false,
      disabledTags: [],
      connectedClients: 3,
      connectedClientsByScope: {},
      totalInvalidations: 12,
      recentInvalidations: [],
    });

    const { TestProvider } = createTestProvider({ client });

    render(<StatusWidget />, { wrapper: TestProvider });

    expect(await screen.findByText("3")).toBeInTheDocument();
  });
});
Call client.reset() between test cases to clear all spy call history without recreating the client. Access individual spies via client.mocks.getStatus, client.mocks.pause, etc.

Testing Scoped Invalidation

When your app uses scopes (per-user or per-tenant isolation), createMockInvalidationSystem records the scope key on each message. Inspect system.emittedMessages directly for assertions beyond the built-in helpers.

Asserting scope key in emitted messages
// Testing scoped invalidation (e.g. per-user or per-tenant)
import { createMockInvalidationSystem } from "@tanstack-tools/query-tags/testing";

const system = createMockInvalidationSystem();

await system.invalidateTags({
  tags: [appTags.todos.list()],
  scope: { userId: "user-123" },
});

// Scoped invalidation messages include the scope key
const [msg] = system.emittedMessages;
console.log(msg.scopeKey); // "user:user-123" (or however getScopeKey formats it)

// Scope presence can be asserted via raw emittedMessages
expect(msg.scopeKey).not.toBe("global");

Quick Reference

ScenarioUtilityHow to use
Assert mutation calls invalidateTagscreateMockInvalidationSystemInject mock; call expectTagsInvalidated()
Count invalidation callscreateMockInvalidationSystemgetInvalidationCount(tag)
Reset between testscreateMockInvalidationSystemsystem.reset()
Trigger SSE event in a component testsimulateInvalidationPass runtime from createTestProvider
Mock getStatus / pause / resumecreateMockInvalidatorClientclient.mocks.getStatus.mockResolvedValue(…)
Wrap component in TagInvalidationProvidercreateTestProviderrender(<Comp />, { wrapper: TestProvider })

FAQ

Tags are normalized with stableSerialize() before storage. String tags become JSON strings: "todos" "todos". Array tags become JSON arrays: ['["todo",42]']. Pass the serialized form to the assertion helper.