Nx: migrate Jest to Vitest for NestJS

Nx: migrate Jest to Vitest for NestJS

Overview

Migrating from Jest to Vitest in a NestJS project can significantly improve testing efficiency and performance. This article will guide you through the migration process, focusing on projects using the Nx monorepo structure.

NestJS projects typically use Jest as the default testing framework. However, Vitest has emerged as a mature alternative that offers several advantages:

  • Improved performance through Vite's Hot Module Replacement (HMR)
  • Compatibility with Jest, allowing for easier migration
  • Faster test execution times
  • Better integration with modern JavaScript tooling

We'll explore each concept step-by-step, providing practical examples and best practices along the way. By the end of this article, you'll understand how to:

  1. Set up Vitest in an Nx monorepo
  2. Configure Vitest for optimal performance
  3. Migrate existing Jest tests to Vitest

Monorepo

A monorepo is a version control strategy where multiple projects are stored in a single repository. This approach offers several benefits:

  • Simplified code sharing and reuse across projects
  • Easier management of dependencies and versioning
  • Improved collaboration between teams working on related projects
  • Streamlined CI/CD processes

For example, a monorepo could contain both a frontend React application and a backend NestJS API, allowing developers to manage the entire stack in one place. This structure facilitates easier updates and ensures consistency across the entire application ecosystem.

Nx

Nx is a powerful monorepo management tool that enhances development workflows in large-scale projects. Key features of Nx include:

  • Consistent project structure and tooling across the monorepo
  • Efficient dependency graph calculation for optimized builds and testing
  • Advanced caching mechanisms to speed up repetitive tasks
  • Extensible plugin system supporting various frameworks and tools

Nx's popularity stems from its ability to provide a unified interface for interacting with different frameworks, making it easier to manage complex, multi-project repositories. The version used in this article (18+) offers improved performance and additional features compared to earlier versions.

Vitest

Vitest is a next-generation testing framework designed to work seamlessly with Vite. Its key advantages include:

  • Exceptional performance through Vite's Hot Module Replacement (HMR)
  • Jest-compatible API, allowing for easier migration from Jest
  • Native TypeScript support without additional configuration
  • Built-in code coverage reporting
  • Multi-threaded test execution for faster results

Vitest leverages Vite's HMR capabilities to provide near-instantaneous feedback during test runs. This speed improvement, combined with its compatibility with Jest, makes it an attractive option for projects looking to enhance their testing workflow without a complete overhaul of existing test suites.

@nx/vite

Workspace

Run nx add @nx/vite to install vite and run nx g vitest --project <projectName> to generate relevant files. With Nx, there could be more libraries to be tested and we can state it in the vitest.workspace.ts

export default ['**/*/vite.config.ts', '**/*/vitest.config.ts']; // by default
// import { defineWorkspace } from 'vitest/config';

// export default defineWorkspace([
  // 'apps/*',
  // 'libs/*',
  // // Add more patterns as needed
// ]);        

Vite config or Vitest config

The default setup is:

/// <reference types='vitest' />
import { defineConfig } from 'vite';

import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

export default defineConfig({
  root: __dirname,
  cacheDir: '../node_modules/.vite/nest-backend',

  plugins: [nxViteTsPaths()],

  // Uncomment this if you are using workers.
  // worker: {
  //  plugins: [ nxViteTsPaths() ],
  // },

  test: {
    globals: true,
    cache: {
      dir: '../node_modules/.vitest/nest-backend',
    },
    environment: 'jsdom',
    include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],

    reporters: ['default'],
    coverage: {
      reportsDirectory: '../coverage/nest-backend',
      provider: 'v8',
    },
  },
});        

Here’s another project using vitest.config.ts. I use extra swc to further improve the performance.

import swc from 'unplugin-swc';
import { defineConfig } from 'vitest/config';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: [
      '**/*.{test,spec,e2e-spec}.?(c|m)[jt]s?(x)',
      './test/**/*.e2e-spec.ts',
    ],
    reporters: ['verbose'],
    testTimeout: 120000,
  },
  plugins: [
    nxViteTsPaths(),
    swc.vite({
      module: { type: 'es6' },
    }),
  ],
});        

NestJS Migration

Originally, the Jest implementations:

import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/sequelize';
import { TaskService } from './task.service';
import { Task } from './entities/task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

describe('TaskService', () => {
  let service: TaskService;
  let mockTaskRepository: any;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [TaskService],
    })
      .useMocker((token) => {
        if (token === getModelToken(Task)) {
          return {
            create: jest.fn(),
            findAll: jest.fn(),
            findOne: jest.fn(),
            update: jest.fn(),
            destroy: jest.fn(),
          };
        }
        
        if (typeof token === 'function') {
          const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
          const Mock = moduleMocker.generateFromMetadata(mockMetadata);
          return new Mock();
        }
      })
      .compile();
    service = module.get<TaskService>(TaskService);
    mockTaskRepository = module.get(getModelToken(Task));
  });
  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});        

It is easy as that:

import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/sequelize';
import { TaskService } from './task.service';
import { Task } from './entities/task.entity';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { describe, expect, it, vi } from 'vitest'; // import it explictily

describe('TaskService', () => {
  let service: TaskService;
  let mockTaskRepository: any;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [TaskService],
    })
      .useMocker((token) => {
        if (token === getModelToken(Task)) {
          return {
            create: vi.fn(), // replace jest to vi
            findAll: vi.fn(),
            findOne: vi.fn(),
            update: vi.fn(),
            destroy: vi.fn(),
          };
        }

        if (typeof token === 'function') {
          return vi.fn();
        }
      })
      .compile();
    service = module.get<TaskService>(TaskService);
    mockTaskRepository = module.get(getModelToken(Task));
  });
  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});        

The main differences are replacing the moduleMocker and when the token equals function.

Conclusion

Migrating from Jest to Vitest is straightforward. The main differences involve replacing Jest-specific functions like moduleMocker with their Vitest counterparts, such as using vi.fn() instead of jest.fn(). The process is relatively simple and improves performance due to Vitest's efficiency, making it a worthwhile migration.

要查看或添加评论,请登录

Guan Xin Wang的更多文章

社区洞察

其他会员也浏览了