NestJS单元测试 #

测试概述 #

NestJS提供了完善的测试支持,默认使用Jest作为测试框架。单元测试用于测试独立的代码单元,如服务、控制器等。

Jest配置 #

jest.config.js #

javascript
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',
};

测试服务 #

基本服务测试 #

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

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

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

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

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

模拟依赖 #

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

describe('UsersService', () => {
  let service: UsersService;
  let repository: jest.Mocked<UsersRepository>;

  const mockRepository = {
    find: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    save: jest.fn(),
    delete: jest.fn(),
  };

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

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

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

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

      mockRepository.find.mockResolvedValue(mockUsers);

      const result = await service.findAll();

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

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

      const result = await service.findOne(1);

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

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

      const result = await service.findOne(999);

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

测试异常 #

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

    mockRepository.findOne.mockResolvedValue({ id: 1, email: createUserDto.email });

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

  it('should create a new user', async () => {
    const createUserDto = {
      name: 'John',
      email: 'john@example.com',
      password: 'password',
    };

    mockRepository.findOne.mockResolvedValue(null);
    mockRepository.create.mockReturnValue({ id: 1, ...createUserDto });
    mockRepository.save.mockResolvedValue({ id: 1, ...createUserDto });

    const result = await service.create(createUserDto);

    expect(result).toEqual({ id: 1, ...createUserDto });
  });
});

测试控制器 #

基本控制器测试 #

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

describe('UsersController', () => {
  let controller: UsersController;
  let service: jest.Mocked<UsersService>;

  const mockService = {
    findAll: jest.fn(),
    findOne: jest.fn(),
    create: jest.fn(),
    update: jest.fn(),
    remove: 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);
  });

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

      service.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' };
      service.findOne.mockResolvedValue(mockUser);

      const result = await controller.findOne(1);

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

测试守卫和装饰器 #

typescript
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';

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

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

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

  it('should bypass JwtAuthGuard', () => {
    expect(controller).toBeDefined();
  });
});

测试管道 #

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

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

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

    pipe = module.get<ValidationPipe>(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();
  });
});

测试拦截器 #

typescript
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();
      },
    });
  });
});

测试守卫 #

typescript
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 allow access when user has required role', () => {
    jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);

    const context = {
      switchToHttp: () => ({
        getRequest: () => ({ user: { role: 'admin' } }),
      }),
      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);
  });
});

测试数据库操作 #

使用内存数据库 #

typescript
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule, getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';

describe('UsersService (Integration)', () => {
  let service: UsersService;
  let repository: Repository<User>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          entities: [User],
          synchronize: true,
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UsersService],
    }).compile();

    service = module.get<UsersService>(UsersService);
    repository = module.get<Repository<User>>(getRepositoryToken(User));
  });

  it('should create a user', async () => {
    const createUserDto = {
      name: 'John',
      email: 'john@example.com',
      password: 'password',
    };

    const result = await service.create(createUserDto);

    expect(result).toHaveProperty('id');
    expect(result.name).toBe(createUserDto.name);
    expect(result.email).toBe(createUserDto.email);
  });
});

测试覆盖率 #

运行覆盖率测试 #

bash
npm run test:cov

覆盖率报告 #

text
------------------------|----------|----------|----------|----------|
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  |
 auth                   |    85.0  |    80.0  |    85.7  |    84.5  |
  auth.service.ts       |    85.0  |    80.0  |    85.7  |    84.5  |
------------------------|----------|----------|----------|----------|

最佳实践 #

1. 使用describe分组 #

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

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

2. 清理模拟 #

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

3. 使用类型安全的模拟 #

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

总结 #

本章学习了NestJS单元测试:

  • Jest配置
  • 服务测试
  • 控制器测试
  • 管道、守卫、拦截器测试
  • 数据库测试
  • 测试覆盖率

接下来,让我们学习 E2E测试

最后更新:2026-03-28