DEV Community

Digital dev
Digital dev

Posted on

Testing After Migration: Adapting Your Vitest Setup to Work With Next.js

The Migration Gap: Why Your Tests Break

Moving from a Vite-based Single Page Application (SPA) to Next.js is a significant architectural shift. While Vite focuses on lightning-fast development using native ESM, Next.js introduces a complex build pipeline involving both client and server-side rendering (SSR), alongside its proprietary routing system.

If you have an extensive suite of unit and integration tests written in Vitest, you might notice things start failing immediately after the move. This isn't because Vitest is incompatible with Next.js, but rather because the global environment your components expect has changed.

In this guide, we will walk through how to configure Vitest to handle Next.js features like the App Router, image optimization, and environment variables.

1. Keep Vitest or Switch to Jest?

Before diving into the configuration, many developers ask if they should switch to Jest, given it was the traditional recommendation for Next.js. However, Vitest has significantly better performance and shares a similar API. If you are migrating a React project, keeping Vitest allows you to reuse 90% of your existing test code.

If you used an automated tool like ViteToNext.AI to handle the bulk of your component and logic migration, your primary task now is simply ensuring the testing environment mirrors the new Next.js runtime.

2. Setting Up the Vitest Configuration

Next.js does not use a native vite.config.ts, so you need to create or update a vitest.config.ts file in your root directory. The key is to use the @vitejs/plugin-react (or the swc version) to transform your JSX specifically for the test environment.

import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: './vitest.setup.ts',
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

3. Mocking Next.js Specific Modules

Next.js modules like next/navigation or next/image do not function outside of the Next.js runtime. When running Vitest, these imports will either throw errors or return undefined.

Mocking the App Router

Since components often use useRouter, useSearchParams, or usePathname, you must mock these hooks in your vitest.setup.ts file:

import { vi } from 'vitest';

vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    prefetch: vi.fn(),
    back: vi.fn(),
  }),
  useSearchParams: () => new URLSearchParams(),
  usePathname: () => '/',
}));
Enter fullscreen mode Exit fullscreen mode

Handling the Next Image Component

The next/image component performs optimizations that aren't necessary for unit tests. You can replace it with a standard img tag to avoid errors regarding missing loaders:

vi.mock('next/image', () => ({
  __esModule: true,
  default: (props: any) => {
    // eslint-disable-next-line @next/next/no-img-element
    return <img {...props} alt={props.alt} />;
  },
}));
Enter fullscreen mode Exit fullscreen mode

4. Addressing CSS and Asset Imports

In a Vite project, you might have relied on Vite's default handling of CSS modules. In Next.js, while the syntax is similar, the way classes are hashed might differ. To prevent Vitest from trying to parse CSS files as JavaScript, modify your config to stub them out:

// vitest.config.ts updates
export default defineConfig({
  test: {
    css: true, // or use a mock if you want to speed up tests
  },
});
Enter fullscreen mode Exit fullscreen mode

5. Environment Variables

Next.js prefixes public variables with NEXT_PUBLIC_. If your old Vite project used VITE_, you likely renamed these during migration. Vitest needs to be aware of these. You can load them manually in your setup file using dotenv or simply define them in the define section of your Vitest config:

defineConfig({
  test: {
    env: {
      NEXT_PUBLIC_API_URL: 'http://localhost:3000/api',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

6. Testing Server Components vs Client Components

One of the biggest hurdles is that Vitest (with JSDOM) is designed for Client Components. Testing Server Components directly in Vitest is tricky because they are async and expect a Node context without browser APIs.

For Server Components, it is often better to:

  1. Extract Logic: Move the data fetching or transformation logic into separate, pure TypeScript functions that can be unit tested easily.
  2. End-to-End Testing: Use Playwright or Cypress for components that rely heavily on the Next.js server runtime.

Conclusion

Adapting Vitest for Next.js is primarily about bridging the gap between the expected browser environment and the specific APIs Next.js provides. By mocking the router, handling images, and ensuring your aliases are correctly mapped, you can maintain a high-velocity development cycle without sacrificing the reliability of your existing test suite.

Further reading: Learn how to automate your Vite to Next.js transition at vitetonext.codebypaki.online

Top comments (1)

Collapse
 
raju_dandigam profile image
Raju Dandigam

Keeping Vitest is the pragmatic call. Most migration pain is not the assertion API, it is the environment contract changing underneath the same tests once routing, image handling, and server or client boundaries show up.

One thing that has helped us is splitting pure component tests from framework-integration tests early. Otherwise every mock starts pretending to be Next.js and the suite gets fragile fast.

Did you end up needing different test environments for App Router server components versus client components, or was one config enough?