首页 / NestJS 入门教程 / 单元测试

NestJS 入门教程

单元测试

本教程共 47 篇 · 第 44 篇 · 更新于 2026-08-09 · 约 14 分钟阅读

NestJS单元测试JestMock测试覆盖率

本节目标:掌握NestJS单元测试的核心技能,学会用Jest测试服务、控制器、守卫、拦截器等组件,写出可靠的测试用例。

写代码不写测试,就像开车不系安全带。平时感觉没啥,出了事就后悔。NestJS内置了对Jest的支持,写测试非常方便。

Jest基础配置

用Nest CLI创建的项目已经自带Jest配置。看看jest.config.js

module.exports = {
  moduleFileExtensions: ['js', 'json', 'ts'],
  rootDir: 'src',
  testRegex: '.*\\.spec\\.ts$',
  transform: {
    '^.+\\.(t|j)s$': 'ts-jest',
  },
  collectCoverageFrom: ['**/*.(t|j)s'],
  coverageDirectory: '../coverage',
  testEnvironment: 'node',
};

几个关键配置:

  • testRegex:匹配.spec.ts结尾的测试文件
  • transform:用ts-jest编译TypeScript
  • testEnvironment:Node.js环境

package.json里的测试脚本:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
  }
}
  • test:运行所有测试
  • test:watch:监听模式,改代码自动重测
  • test:cov:生成覆盖率报告

测试服务

服务是业务逻辑的核心,测试服务最重要。

基本测试

假设有个UsersService

@Injectable()
export class UsersService {
  constructor(private userRepository: UserRepository) {}

  async findAll(): Promise<User[]> {
    return this.userRepository.find();
  }

  async findOne(id: number): Promise<User> {
    return this.userRepository.findOne({ where: { id } });
  }
}

写测试:

import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { UserRepository } from './users.repository';

describe('UsersService', () => {
  let service: UsersService;
  let repository: UserRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: UserRepository,
          useValue: {
            find: jest.fn(),
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    repository = module.get<UserRepository>(UserRepository);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});

Test.createTestingModule()创建测试模块,跟@Module()装饰器的参数一样。用provideuseValue提供Mock的依赖。

测试方法

describe('findAll', () => {
  it('should return an array of users', async () => {
    const mockUsers = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' },
    ];

    // 模拟repository.find()的返回值
    jest.spyOn(repository, 'find').mockResolvedValue(mockUsers);

    const result = await service.findAll();

    expect(result).toEqual(mockUsers);
    expect(repository.find).toHaveBeenCalled();
  });
});

describe('findOne', () => {
  it('should return a user by id', async () => {
    const mockUser = { id: 1, name: 'John' };
    jest.spyOn(repository, 'findOne').mockResolvedValue(mockUser);

    const result = await service.findOne(1);

    expect(result).toEqual(mockUser);
    expect(repository.findOne).toHaveBeenCalledWith({ where: { id: 1 } });
  });

  it('should return null if user not found', async () => {
    jest.spyOn(repository, 'findOne').mockResolvedValue(null);

    const result = await service.findOne(999);

    expect(result).toBeNull();
  });
});

jest.spyOn()拦截方法调用,mockResolvedValue()设置异步返回值。

测试异常

describe('create', () => {
  it('should throw ConflictException if email exists', async () => {
    const createUserDto = {
      name: 'John',
      email: 'john@example.com',
      password: 'password',
    };

    jest.spyOn(repository, 'findOne').mockResolvedValue({
      id: 1,
      email: createUserDto.email,
    });

    await expect(service.create(createUserDto))
      .rejects.toThrow(ConflictException);
  });
});

rejects.toThrow()测试异步方法是否抛出异常。

测试控制器

控制器测试跟服务测试类似,但要把控制器的依赖Mock掉。

import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
  let controller: UsersController;
  let service: UsersService;

  const mockService = {
    findAll: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [
        {
          provide: UsersService,
          useValue: mockService,
        },
      ],
    }).compile();

    controller = module.get<UsersController>(UsersController);
    service = module.get<UsersService>(UsersService);
  });

  describe('findAll', () => {
    it('should return an array of users', async () => {
      const mockUsers = [{ id: 1, name: 'John' }];
      mockService.findAll.mockResolvedValue(mockUsers);

      const result = await controller.findAll();

      expect(result).toEqual(mockUsers);
    });
  });

  describe('findOne', () => {
    it('should return a user by id', async () => {
      const mockUser = { id: 1, name: 'John' };
      mockService.findOne.mockResolvedValue(mockUser);

      const result = await controller.findOne(1);

      expect(result).toEqual(mockUser);
      expect(mockService.findOne).toHaveBeenCalledWith(1);
    });
  });
});

控制器测试重点验证它是否正确调用了服务方法,以及返回值是否正确。

覆盖依赖

有时候不想Mock整个服务,只想替换某个特定的依赖。用overrideProvider()

const module: TestingModule = await Test.createTestingModule({
  imports: [UsersModule],
})
  .overrideProvider(UsersService)
  .useValue({
    findAll: () => ['test'],
  })
  .compile();

overrideProvider()返回一个对象,有三个方法:

  • useValue:提供一个固定值
  • useClass:提供一个类
  • useFactory:提供一个工厂函数

类似地,还有:

  • overrideGuard():覆盖守卫
  • overrideInterceptor():覆盖拦截器
  • overridePipe():覆盖管道
  • overrideFilter():覆盖过滤器

覆盖守卫

const module: TestingModule = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    { provide: UsersService, useValue: {} },
  ],
})
  .overrideGuard(JwtAuthGuard)
  .useValue({ canActivate: () => true })
  .compile();

这样就能绕过JWT认证,不用在测试里搞真的Token。

测试守卫

守卫的核心是canActivate()方法,测试它返回true还是false。

import { Test, TestingModule } from '@nestjs/testing';
import { RolesGuard } from './roles.guard';
import { ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

describe('RolesGuard', () => {
  let guard: RolesGuard;
  let reflector: Reflector;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [RolesGuard, Reflector],
    }).compile();

    guard = module.get<RolesGuard>(RolesGuard);
    reflector = module.get<Reflector>(Reflector);
  });

  it('should allow access when no roles required', () => {
    jest.spyOn(reflector, 'getAllAndOverride')
      .mockReturnValue(undefined);

    const context = {
      switchToHttp: () => ({
        getRequest: () => ({ user: { role: 'user' } }),
      }),
      getHandler: () => jest.fn(),
      getClass: () => jest.fn(),
    } as unknown as ExecutionContext;

    expect(guard.canActivate(context)).toBe(true);
  });

  it('should deny access when user lacks required role', () => {
    jest.spyOn(reflector, 'getAllAndOverride')
      .mockReturnValue(['admin']);

    const context = {
      switchToHttp: () => ({
        getRequest: () => ({ user: { role: 'user' } }),
      }),
      getHandler: () => jest.fn(),
      getClass: () => jest.fn(),
    } as unknown as ExecutionContext;

    expect(() => guard.canActivate(context))
      .toThrow(ForbiddenException);
  });
});

Mock一个假的ExecutionContext,模拟不同的请求场景。

测试拦截器

拦截器返回Observable,测试时用of()创建测试流。

import { Test, TestingModule } from '@nestjs/testing';
import { TransformInterceptor } from './transform.interceptor';
import { ExecutionContext, CallHandler } from '@nestjs/common';
import { of } from 'rxjs';

describe('TransformInterceptor', () => {
  let interceptor: TransformInterceptor<any>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [TransformInterceptor],
    }).compile();

    interceptor = module.get<TransformInterceptor<any>>(
      TransformInterceptor
    );
  });

  it('should transform response', (done) => {
    const context = {
      switchToHttp: () => ({
        getRequest: () => ({}),
        getResponse: () => ({}),
      }),
    } as ExecutionContext;

    const next: CallHandler = {
      handle: () => of({ name: 'John' }),
    };

    interceptor.intercept(context, next).subscribe({
      next: (result) => {
        expect(result).toEqual({
          code: 200,
          message: 'Success',
          data: { name: 'John' },
          timestamp: expect.any(String),
        });
        done();
      },
    });
  });
});

done回调告诉Jest这是异步测试,等Observable发出数据后再断言。

测试管道

管道测试相对简单,直接调用transform()方法。

import { ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

describe('CreateUserDto Validation', () => {
  let pipe: ValidationPipe;

  beforeEach(() => {
    pipe = new ValidationPipe();
  });

  it('should pass validation with valid data', async () => {
    const dto = {
      name: 'John',
      email: 'john@example.com',
      password: 'password123',
    };

    const result = await pipe.transform(dto, {
      type: 'body',
      metatype: CreateUserDto,
    });

    expect(result).toEqual(dto);
  });

  it('should fail validation with invalid email', async () => {
    const dto = {
      name: 'John',
      email: 'invalid-email',
      password: 'password123',
    };

    await expect(
      pipe.transform(dto, {
        type: 'body',
        metatype: CreateUserDto,
      })
    ).rejects.toThrow();
  });
});

自动Mock

依赖太多时,一个个Mock太麻烦。用useMocker()自动Mock所有未提供的依赖:

import { ModuleMocker, MockMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

describe('CatsController', () => {
  let controller: CatsController;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
    })
      .useMocker((token) => {
        // 特定服务的Mock
        if (token === CatsService) {
          return { 
            findAll: jest.fn().mockResolvedValue(['test1', 'test2']) 
          };
        }
        // 其他依赖自动生成Mock
        if (typeof token === 'function') {
          const mockMetadata = moduleMocker.getMetadata(
            token
          ) as MockMetadata<any, any>;
          const Mock = moduleMocker.generateFromMetadata(mockMetadata);
          return new Mock();
        }
      })
      .compile();

    controller = moduleRef.get(CatsController);
  });
});

useMocker()接收一个工厂函数,对每个未提供的依赖自动创建Mock。

测试覆盖率

运行覆盖率测试:

npm run test:cov

会生成覆盖率报告:

------------------------|----------|----------|----------|----------|
File                    |  % Stmts | % Branch |  % Funcs |  % Lines |
------------------------|----------|----------|----------|----------|
All files               |    90.5  |    85.2  |    92.3  |    90.1  |
 users                  |    95.2  |    90.0  |   100.0  |    95.0  |
  users.controller.ts   |   100.0  |   100.0  |   100.0  |   100.0  |
  users.service.ts      |    92.3  |    85.7  |   100.0  |    92.0  |
------------------------|----------|----------|----------|----------|

四个指标:

  • Stmts(语句覆盖率):执行了多少语句
  • Branch(分支覆盖率):if/else走了多少分支
  • Funcs(函数覆盖率):调用了多少函数
  • Lines(行覆盖率):执行了多少行代码
Tip

覆盖率不是越高越好,但80%以上是个合理的目标。别为了凑数字写无意义的测试。

最佳实践

用describe分组

describe('UsersService', () => {
  describe('findAll', () => {
    it('should return users', () => {});
  });

  describe('findOne', () => {
    it('should return user', () => {});
    it('should return null if not found', () => {});
  });
});

按方法分组,测试报告更清晰。

清理Mock

afterEach(() => {
  jest.clearAllMocks();
});

每个测试后清理Mock状态,避免测试间互相影响。

类型安全的Mock

const mockService = {
  findAll: jest.fn() as jest.MockedFunction<UsersService['findAll']>,
};

这样Mock就有正确的类型签名,IDE能给你提示。

测试文件命名

测试文件放在被测试文件旁边,命名为xxx.spec.ts。比如users.service.ts对应users.service.spec.ts

Note

单元测试只测试单个类或方法,把所有依赖都Mock掉。如果想测试整个模块的集成,用E2E测试(下一章)。

小结

这一章学了NestJS单元测试的核心技能:

Jest配置:Nest CLI已经帮你搞好了,直接用就行。

测试服务:用Test.createTestingModule()创建测试模块,Mock掉依赖,测试业务逻辑。

测试控制器:Mock服务,验证控制器是否正确调用服务方法。

覆盖依赖:用overrideProvider()等方法替换特定的依赖。

测试守卫、拦截器、管道:MockExecutionContextCallHandler,测试它们的逻辑。

自动Mock:依赖太多时用useMocker()自动生成Mock。

测试覆盖率:用npm run test:cov查看覆盖率报告。

单元测试是代码质量的保障。写好单元测试,改代码的时候心里有底,不怕改坏。下一章我们聊E2E测试。