Skip to content
On this page

NestJS 测试 (Testing)

NestJS 提供了强大的测试工具和框架集成,支持单元测试、集成测试和端到端测试。NestJS 的测试功能基于 Jest(默认)和 Supertest,同时也支持其他测试框架如 Mocha。

基础概念

NestJS 测试包含:

  1. 单元测试 - 测试单个类、方法或函数
  2. 集成测试 - 测试多个组件之间的交互
  3. 端到端测试 - 测试整个应用程序流程
  4. 测试工具 - 提供测试实用工具和模拟功能

测试环境设置

安装测试依赖

bash
npm install --save-dev jest @types/jest supertest @types/supertest

Jest 配置

json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "collectCoverageFrom": [
    "**/*.(t|j)s",
    "!**/node_modules/**",
    "!**/coverage/**",
    "!**/dist/**"
  ]
}

单元测试

服务单元测试

typescript
// cats.service.ts
import { Injectable } from '@nestjs/common';

export interface Cat {
  id: number;
  name: string;
  breed: string;
}

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat): Cat {
    this.cats.push(cat);
    return cat;
  }

  findAll(): Cat[] {
    return this.cats;
  }

  findOne(id: number): Cat {
    return this.cats.find(cat => cat.id === id);
  }
}
typescript
// cats.service.spec.ts
import { Test } from '@nestjs/testing';
import { CatsService } from './cats.service';

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

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

    service = moduleRef.get<CatsService>(CatsService);
  });

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

  describe('create', () => {
    it('should add a cat to the array', () => {
      const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
      const result = service.create(cat);
      expect(result).toEqual(cat);
      expect(service.findAll()).toContain(cat);
    });
  });

  describe('findAll', () => {
    it('should return an array of cats', () => {
      const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
      service.create(cat);
      expect(service.findAll()).toEqual([cat]);
    });
  });

  describe('findOne', () => {
    it('should return a specific cat', () => {
      const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
      service.create(cat);
      expect(service.findOne(1)).toEqual(cat);
    });

    it('should return undefined for non-existent cat', () => {
      expect(service.findOne(999)).toBeUndefined();
    });
  });
});

控制器单元测试

typescript
// cats.controller.ts
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CatsService, Cat } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Post()
  create(@Body() createCatDto: Cat) {
    return this.catsService.create(createCatDto);
  }

  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.catsService.findOne(+id);
  }
}
typescript
// cats.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

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

  beforeEach(async () => {
    const moduleRef: TestingModule = await Test.createTestingModule({
      controllers: [CatsController],
      providers: [
        {
          provide: CatsService,
          useValue: {
            create: jest.fn(),
            findAll: jest.fn(),
            findOne: jest.fn(),
          },
        },
      ],
    }).compile();

    controller = moduleRef.get<CatsController>(CatsController);
    service = moduleRef.get<CatsService>(CatsService);
  });

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

  describe('create', () => {
    it('should call catsService.create', () => {
      const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
      jest.spyOn(service, 'create').mockReturnValue(cat);

      expect(controller.create(cat)).toEqual(cat);
      expect(service.create).toHaveBeenCalledWith(cat);
    });
  });

  describe('findAll', () => {
    it('should call catsService.findAll', () => {
      const cats = [{ id: 1, name: 'Fluffy', breed: 'Persian' }];
      jest.spyOn(service, 'findAll').mockReturnValue(cats);

      expect(controller.findAll()).toEqual(cats);
      expect(service.findAll).toHaveBeenCalled();
    });
  });

  describe('findOne', () => {
    it('should call catsService.findOne with correct id', () => {
      const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
      jest.spyOn(service, 'findOne').mockReturnValue(cat);

      expect(controller.findOne('1')).toEqual(cat);
      expect(service.findOne).toHaveBeenCalledWith(1);
    });
  });
});

集成测试

模块集成测试

typescript
// cats.module.int-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsModule } from './cats.module';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';

describe('CatsModule Integration', () => {
  let service: CatsService;
  let controller: CatsController;

  beforeEach(async () => {
    const moduleRef: TestingModule = await Test.createTestingModule({
      imports: [CatsModule],
    }).compile();

    service = moduleRef.get<CatsService>(CatsService);
    controller = moduleRef.get<CatsController>(CatsController);
  });

  it('should resolve CatsService and CatsController', () => {
    expect(service).toBeDefined();
    expect(controller).toBeDefined();
  });

  it('should create and retrieve a cat', () => {
    const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
    
    const createdCat = service.create(cat);
    expect(createdCat).toEqual(cat);
    
    const retrievedCat = service.findOne(1);
    expect(retrievedCat).toEqual(cat);
  });
});

依赖注入测试

typescript
// database.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class DatabaseService {
  async connect() {
    // 连接数据库
    return true;
  }

  async disconnect() {
    // 断开数据库连接
    return true;
  }
}

// user.service.ts
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class UserService {
  constructor(@Inject('DATABASE_SERVICE') private dbService: any) {}

  async createUser(userData: any) {
    await this.dbService.connect();
    // 创建用户逻辑
    return { id: 1, ...userData };
  }
}
typescript
// user.service.spec.ts
import { Test } from '@nestjs/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let mockDbService: any;

  beforeEach(async () => {
    mockDbService = {
      connect: jest.fn().mockResolvedValue(true),
      disconnect: jest.fn().mockResolvedValue(true),
    };

    const moduleRef = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: 'DATABASE_SERVICE',
          useValue: mockDbService,
        },
      ],
    }).compile();

    service = moduleRef.get<UserService>(UserService);
  });

  it('should create a user', async () => {
    const userData = { name: 'John', email: 'john@example.com' };
    const result = await service.createUser(userData);

    expect(mockDbService.connect).toHaveBeenCalled();
    expect(result).toEqual({ id: 1, ...userData });
  });
});

端到端测试

应用程序端到端测试

typescript
// app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });

  afterAll(async () => {
    await app.close();
  });
});

控制器端到端测试

typescript
// cats.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { CatsModule } from '../src/cats/cats.module';

describe('CatsController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [CatsModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/cats (GET)', () => {
    return request(app.getHttpServer())
      .get('/cats')
      .expect(200)
      .expect([]);
  });

  it('/cats (POST)', () => {
    return request(app.getHttpServer())
      .post('/cats')
      .send({ name: 'Fluffy', breed: 'Persian' })
      .expect(201)
      .expect({
        id: expect.any(Number),
        name: 'Fluffy',
        breed: 'Persian',
      });
  });

  it('/cats/:id (GET)', () => {
    return request(app.getHttpServer())
      .post('/cats')
      .send({ name: 'Fluffy', breed: 'Persian' })
      .then(response => {
        const id = response.body.id;
        return request(app.getHttpServer())
          .get(`/cats/${id}`)
          .expect(200)
          .expect({
            id: id,
            name: 'Fluffy',
            breed: 'Persian',
          });
      });
  });

  afterAll(async () => {
    await app.close();
  });
});

Mocking 和 Spying

Mocking 服务

typescript
// mocking example
describe('CatsService with mocked dependencies', () => {
  let service: CatsService;
  let loggerService: any;

  beforeEach(async () => {
    loggerService = {
      log: jest.fn(),
      error: jest.fn(),
    };

    const moduleRef = await Test.createTestingModule({
      providers: [
        CatsService,
        {
          provide: 'LoggerService',
          useValue: loggerService,
        },
      ],
    }).compile();

    service = moduleRef.get<CatsService>(CatsService);
  });

  it('should log when creating a cat', () => {
    const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
    service.create(cat);

    expect(loggerService.log).toHaveBeenCalledWith('Cat created', cat);
  });
});

使用 Jest Spies

typescript
describe('Service with spies', () => {
  let service: CatsService;

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

    service = moduleRef.get<CatsService>(CatsService);
  });

  it('should call internal method when creating cat', () => {
    const internalMethodSpy = jest.spyOn(service as any, 'internalValidation');
    
    const cat = { id: 1, name: 'Fluffy', breed: 'Persian' };
    service.create(cat);

    expect(internalMethodSpy).toHaveBeenCalledWith(cat);
  });
});

测试工具

Test.createTestingModule

typescript
// 使用 Test.createTestingModule 创建测试模块
import { Test } from '@nestjs/testing';

describe('Complex service test', () => {
  it('should test service with multiple dependencies', async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [
        CatsService,
        {
          provide: 'DatabaseConnection',
          useValue: mockDatabaseConnection,
        },
        {
          provide: 'ConfigService',
          useValue: mockConfigService,
        },
      ],
    }).compile();

    const service = moduleRef.get<CatsService>(CatsService);
    // 执行测试
    expect(service).toBeDefined();
  });
});

overrideProvider

typescript
describe('Service with overridden provider', () => {
  let service: CatsService;

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      providers: [CatsService],
    })
    .overrideProvider('DatabaseConnection')
    .useValue(mockDatabaseConnectionWithFailures)
    .compile();

    service = moduleRef.get<CatsService>(CatsService);
  });

  it('should handle database failures gracefully', async () => {
    // 测试错误处理逻辑
    await expect(service.findAll()).rejects.toThrow();
  });
});

异步测试

异步服务测试

typescript
// async service
@Injectable()
export class AsyncCatsService {
  async createAsync(cat: Cat): Promise<Cat> {
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 100));
    return { ...cat, id: Date.now() };
  }

  async findAllAsync(): Promise<Cat[]> {
    await new Promise(resolve => setTimeout(resolve, 50));
    return [];
  }
}

// async test
describe('AsyncCatsService', () => {
  let service: AsyncCatsService;

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

    service = moduleRef.get<AsyncCatsService>(AsyncCatsService);
  });

  it('should create cat asynchronously', async () => {
    const cat = { name: 'Fluffy', breed: 'Persian' };
    const result = await service.createAsync(cat);
    
    expect(result).toBeDefined();
    expect(result.name).toBe('Fluffy');
    expect(result.breed).toBe('Persian');
    expect(result.id).toBeDefined();
  });

  it('should find all cats asynchronously', async () => {
    const result = await service.findAllAsync();
    expect(result).toEqual([]);
  });
});

使用 done 回调

typescript
describe('Service with done callback', () => {
  let service: AsyncCatsService;

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

    service = moduleRef.get<AsyncCatsService>(AsyncCatsService);
  });

  it('should handle async operation with done callback', (done) => {
    service.createAsync({ name: 'Fluffy', breed: 'Persian' })
      .then(result => {
        expect(result).toBeDefined();
        done();
      })
      .catch(done);
  });
});

测试数据库集成

内存数据库测试

typescript
// 使用 SQLite 内存数据库进行测试
import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from '../src/cat.entity';
import { CatsService } from '../src/cats.service';

describe('CatsService with database', () => {
  let service: CatsService;

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

    service = moduleRef.get<CatsService>(CatsService);
  });

  it('should save cat to database', async () => {
    const cat = { name: 'Fluffy', breed: 'Persian' };
    const savedCat = await service.create(cat);

    expect(savedCat.id).toBeDefined();
    expect(savedCat.name).toBe('Fluffy');
  });
});

测试微服务

微服务测试

typescript
// math.controller.ts (微服务)
import { Controller } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  sum(data: number[]): number {
    return (data || []).reduce((a, b) => a + b, 0);
  }
}

// math.controller.spec.ts
import { Test } from '@nestjs/testing';
import { MathController } from './math.controller';

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

  beforeEach(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [MathController],
    }).compile();

    controller = moduleRef.get<MathController>(MathController);
  });

  it('should sum an array of numbers', () => {
    const result = controller.sum([1, 2, 3, 4, 5]);
    expect(result).toBe(15);
  });

  it('should return 0 for empty array', () => {
    const result = controller.sum([]);
    expect(result).toBe(0);
  });
});

测试管道、守卫、拦截器和过滤器

管道测试

typescript
// validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ValidationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (!value) {
      throw new BadRequestException('Value is required');
    }
    return value;
  }
}

// validation.pipe.spec.ts
import { ValidationPipe } from './validation.pipe';

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

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

  it('should return the value if it exists', () => {
    const value = { name: 'Fluffy' };
    expect(pipe.transform(value, {} as ArgumentMetadata)).toEqual(value);
  });

  it('should throw an error if value is empty', () => {
    expect(() => {
      pipe.transform(null, {} as ArgumentMetadata);
    }).toThrow(BadRequestException);
  });
});

守卫测试

typescript
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return request.headers.authorization === 'Bearer valid-token';
  }
}

// auth.guard.spec.ts
import { AuthGuard } from './auth.guard';

describe('AuthGuard', () => {
  let guard: AuthGuard;

  beforeEach(() => {
    guard = new AuthGuard();
  });

  it('should allow access with valid token', () => {
    const context = {
      switchToHttp: jest.fn(() => ({
        getRequest: jest.fn(() => ({
          headers: { authorization: 'Bearer valid-token' },
        })),
      })),
    } as ExecutionContext;

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

  it('should deny access with invalid token', () => {
    const context = {
      switchToHttp: jest.fn(() => ({
        getRequest: jest.fn(() => ({
          headers: { authorization: 'Bearer invalid-token' },
        })),
      })),
    } as ExecutionContext;

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

测试最佳实践

1. 测试结构

typescript
// 推荐的测试结构
describe('Feature Name', () => {
  // 设置测试环境
  let service: MyService;

  beforeEach(async () => {
    // 准备测试数据
    const moduleRef = await Test.createTestingModule({
      providers: [MyService],
    }).compile();

    service = moduleRef.get<MyService>(MyService);
  });

  // 测试不同的场景
  describe('specific method', () => {
    it('should handle normal case', () => {
      // 测试正常情况
    });

    it('should handle edge case', () => {
      // 测试边界情况
    });

    it('should handle error case', () => {
      // 测试错误情况
    });
  });
});

2. 测试数据工厂

typescript
// test-data.factory.ts
export class TestDataFactory {
  static createCat(overrides: Partial<Cat> = {}): Cat {
    return {
      id: 1,
      name: 'Test Cat',
      breed: 'Test Breed',
      ...overrides,
    };
  }

  static createCats(count: number): Cat[] {
    return Array.from({ length: count }, (_, i) => 
      this.createCat({ id: i + 1, name: `Cat ${i + 1}` })
    );
  }
}

// 在测试中使用
import { TestDataFactory } from './test-data.factory';

describe('CatsService', () => {
  it('should create a cat with custom data', () => {
    const customCat = TestDataFactory.createCat({ name: 'Custom Name' });
    const result = service.create(customCat);
    expect(result.name).toBe('Custom Name');
  });
});

3. 测试覆盖率

json
// 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 --no-cache"
  }
}

4. 测试环境变量

typescript
// test.config.ts
export const testConfig = {
  database: {
    host: process.env.TEST_DB_HOST || 'localhost',
    port: parseInt(process.env.TEST_DB_PORT, 10) || 5432,
  },
  api: {
    baseUrl: process.env.TEST_API_BASE_URL || 'http://localhost:3000',
  },
};

性能测试

基准测试

typescript
describe('Performance Tests', () => {
  let service: CatsService;

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

    service = moduleRef.get<CatsService>(CatsService);
  });

  it('should handle large datasets efficiently', async () => {
    const startTime = Date.now();
    
    // 创建大量数据进行测试
    for (let i = 0; i < 1000; i++) {
      service.create({ id: i, name: `Cat ${i}`, breed: 'Breed' });
    }

    const endTime = Date.now();
    const duration = endTime - startTime;

    // 确保操作在合理时间内完成
    expect(duration).toBeLessThan(1000); // 小于1秒
  });
});

测试运行器配置

Jest 配置高级选项

javascript
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/main.ts',
    '!src/**/index.ts',
  ],
  coverageDirectory: '../coverage',
  coverageReporters: ['text', 'lcov', 'html'],
  testTimeout: 10000, // 10秒超时
  setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
};

CI/CD 集成

GitHub Actions 测试配置

yaml
# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x]
    
    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v2
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run test:cov
    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v1

总结

NestJS 提供了全面的测试工具和框架,支持各种类型的测试。通过适当的测试策略和最佳实践,可以确保应用程序的质量和可靠性。

测试的主要特点:

  • 支持单元、集成和端到端测试
  • 提供强大的测试工具和模拟功能
  • 与主流测试框架集成
  • 支持异步测试
  • 提供测试覆盖率工具
  • 支持各种测试场景(管道、守卫、拦截器等)
  • 包含性能测试方法
  • 支持 CI/CD 集成