Testing Tanstack Start

September 15, 2025
A stylized cube and the words "Testing Tanstack Start Components"

Testing TanStack Start Components: A Practical Mocking Approach

When building knowledgegap.guide, an AI-assisted learning platform, I chose a modern stack: TanStack Start for the full-stack framework, TanStack Query for data fetching, tRPC for type-safe APIs, and Better-Auth for authentication. This combination is fantastic for development, but testing components turned into a puzzle with all these pieces interconnected.

Since there’s no official testing guidance for TanStack Start yet, I figured out an approach that works well for me. Fair warning: this probably doesn’t follow all testing best practices and definitely doesn’t implement the official TanStack Query testing guidance. But it’s straightforward, easy to write, and gets the job done. If you know better ways to test this stack, I’d love to hear from you—hit me up via email or on X @thasmin and I’ll update this post.

The Challenge: Complex Dependencies

TanStack Start components typically depend on several interconnected systems:

Testing these components requires carefully mocking each dependency while maintaining the component’s behavior.

Mocking TanStack Router

TanStack Start uses file-based routing with createFileRoute(). Components rely on router hooks and navigation functionality that don’t work in isolation.

// src/routes/login.tsx
export const Route = createFileRoute("/login")({
  component: RouteComponent,
  loader: async ({ context: { session } }) => {
    if (session && !session.isAnonymous) throw redirect({ href: "/" });
    return { session };
  },
});

function RouteComponent() {
  const router = useRouter();
  const { session } = Route.useLoaderData();
  const { back } = Route.useSearch();
  
  return (
    <Layout>
      <Link to="/signup">Sign up</Link>
      {/* component content */}
    </Layout>
  );
}

The Solution

Mock the router components and hooks with simplified implementations:

// tests/unit/routes/login.test.tsx
vi.mock("@tanstack/react-router", () => ({
  Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
    <a href={to}>{children}</a>
  ),
  useRouter: () => ({
    navigate: vi.fn(),
  }),
  createFileRoute: (_path: string) => (routeOptions: any) => ({
    options: routeOptions,
    useLoaderData: vi.fn(),
    useSearch: vi.fn(),
  }),
}));

Why This Works

  1. Link<a>: Converts complex router links to simple anchor tags that testing-library can find and interact with
  2. useRouter mock: Provides essential methods like navigate() that components expect
  3. createFileRoute mock: Returns a structure that preserves the component while making hooks mockable

Mocking Route-Specific Hooks

File routes create hooks like Route.useLoaderData() and Route.useSearch() that provide server-loaded data and URL parameters.

The Solution

Create a test utility that controls these hooks:

// Test utility for rendering with controlled data
function renderLogin(session: any = null, searchParams: any = {}) {
  // Mock the router hooks that the component uses
  vi.mocked(LoginRoute.useLoaderData).mockReturnValue({ session });
  vi.mocked(LoginRoute.useSearch).mockReturnValue(searchParams);

  const user = userEvent.setup();
  return { user, ...render(<LoginComponent />) };
}

// Usage in tests
it("handles custom redirect URLs", () => {
  renderLogin(null, { back: "/custom-path" });
  // Test that social login uses the custom callback URL
});

Why This Works

Mocking TanStack Query

Components using useQuery depend on TanStack Query’s client and caching system.

// src/routes/certifications.tsx
function RouteComponent() {
  const trpc = useTRPC();
  const titlesQuery = useQuery(trpc.titles.listWithChapters.queryOptions());
  
  return (
    <Layout>
      {titlesQuery.isLoading && <div>Loading...</div>}
      {titlesQuery.data?.map(title => (
        <Link key={title.id} to="/certification/$slug" params={{ slug: title.slug }}>
          {title.name}
        </Link>
      ))}
    </Layout>
  );
}

The Solution

Mock useQuery to control loading states and data:

vi.mock("@tanstack/react-query", async () => {
  const actual = await vi.importActual("@tanstack/react-query");
  return {
    ...actual,
    useQuery: vi.fn(),
  };
});

// Test utility with controlled query states
function renderCertifications(mockQueryState: {
  data?: TitleData[];
  isLoading?: boolean;
  isError?: boolean;
} = {}) {
  vi.mocked(useQuery).mockReturnValue({
    data: mockQueryState.data,
    isLoading: mockQueryState.isLoading ?? false,
    isError: mockQueryState.isError ?? false,
    error: null,
    isSuccess: !mockQueryState.isLoading && !mockQueryState.isError,
    refetch: vi.fn(),
  } as any);

  return render(
    <QueryClientProvider client={queryClient}>
      <CertificationsComponent />
    </QueryClientProvider>
  );
}

// Usage in tests
it("shows loading state", () => {
  renderCertifications({ isLoading: true });
  expect(screen.getByText("Loading...")).toBeInTheDocument();
});

it("renders data when loaded", () => {
  renderCertifications({ data: mockTitlesData });
  expect(screen.getByText("React Fundamentals")).toBeInTheDocument();
});

Why This Works

Mocking tRPC Integration

TanStack Start often integrates with tRPC for type-safe API calls. Components expect tRPC query options and client methods.

The Solution

Mock the tRPC client structure:

vi.mock("/src/trpc/react", () => ({
  useTRPC: () => ({
    titles: {
      listWithChapters: {
        queryOptions: () => ({
          queryKey: ["titles", "listWithChapters"],
          queryFn: vi.fn(),
        }),
      },
    },
  }),
}));

Why This Works

Mocking External Dependencies

Components often depend on utility functions, UI components, and external libraries.

The Solution

Mock each dependency with focused implementations:

// Mock Layout component for testing
vi.mock("/src/components/Layout", () => ({
  default: ({ children, title, icons }: {
    children: React.ReactNode;
    title: string;
    icons?: string;
  }) => (
    <div data-testid="layout" data-title={title} data-icons={icons}>
      {children}
    </div>
  ),
}));

// Mock utility functions
vi.mock("/src/lib/clientUtils", () => ({
  stripedBorderStyle: (color: string) => ({
    border: `2px solid ${color}`,
    backgroundColor: color,
  }),
}));

// Mock authentication client
vi.mock("/src/lib/authClient", () => ({
  authClient: {
    signIn: {
      social: vi.fn().mockResolvedValue({}),
      emailOtp: vi.fn().mockResolvedValue({ success: true }),
      anonymous: vi.fn().mockResolvedValue({}),
    },
    emailOtp: {
      sendVerificationOtp: vi.fn().mockResolvedValue({ success: true }),
    },
  },
}));

Why This Works

Complete Example: Testing a Login Component

Here’s how all these strategies work together:

import { render, screen, waitFor } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock all dependencies
vi.mock("/src/components/Layout", () => ({
  default: ({ children, title }) => (
    <div data-testid="layout" data-title={title}>{children}</div>
  ),
}));

vi.mock("/src/lib/authClient", () => ({
  authClient: {
    signIn: {
      social: vi.fn().mockResolvedValue({}),
      emailOtp: vi.fn().mockResolvedValue({ success: true }),
    },
  },
}));

vi.mock("@tanstack/react-router", () => ({
  Link: ({ children, to }) => <a href={to}>{children}</a>,
  useRouter: () => ({ navigate: vi.fn() }),
  createFileRoute: (_path: string) => (routeOptions: any) => ({
    options: routeOptions,
    useLoaderData: vi.fn(),
    useSearch: vi.fn(),
  }),
}));

import { authClient } from "/src/lib/authClient";
import { Route as LoginRoute } from "/src/routes/login";

const LoginComponent = LoginRoute.options.component;

function renderLogin(session: any = null, searchParams: any = {}) {
  vi.mocked(LoginRoute.useLoaderData).mockReturnValue({ session });
  vi.mocked(LoginRoute.useSearch).mockReturnValue(searchParams);
  
  return { user: userEvent.setup(), ...render(<LoginComponent />) };
}

describe("Login Page", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("renders login form", async () => {
    renderLogin();
    
    await waitFor(() => {
      expect(screen.getByText("Welcome Back")).toBeInTheDocument();
    });
    
    expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
    expect(screen.getByRole("button", { name: /continue with google/i })).toBeInTheDocument();
  });

  it("handles social login", async () => {
    const { user } = renderLogin();
    
    const googleButton = screen.getByRole("button", { name: /continue with google/i });
    await user.click(googleButton);
    
    expect(vi.mocked(authClient.signIn.social)).toHaveBeenCalledWith({
      provider: "google",
      callbackURL: "/todo",
    });
  });

  it("uses custom callback URL", async () => {
    const { user } = renderLogin(null, { back: "/custom-path" });
    
    const googleButton = screen.getByRole("button", { name: /continue with google/i });
    await user.click(googleButton);
    
    expect(vi.mocked(authClient.signIn.social)).toHaveBeenCalledWith({
      provider: "google",
      callbackURL: "/custom-path",
    });
  });
});

Conclusion

This mocking approach for TanStack Start components prioritizes simplicity and practicality:

  1. Simplify complex dependencies while preserving essential behavior
  2. Control data flow with test utilities and mock configurations
  3. Focus on user interactions rather than internal implementation
  4. Handle async operations properly with appropriate waiting

While this approach may not follow all testing best practices (particularly for TanStack Query), it provides fast, reliable tests that catch real bugs and remain maintainable as your application evolves. The key is finding the right balance between realistic behavior and test simplicity.

For the AI-assisted learning platform at knowledgegap.guide, this testing strategy has enabled confident development and refactoring while maintaining good test coverage across complex component interactions.

Feedback Welcome

Since TanStack Start is relatively new and testing patterns are still evolving, I’d love to hear about alternative approaches or improvements to these strategies. Please reach out:

I’ll update this post with better approaches as the ecosystem and best practices continue to mature.