Skip to content

测试金字塔:E2E 测试如何启动完整应用?

在软件开发中,测试是确保代码质量和系统稳定性的关键环节。测试金字塔模型描述了不同层次测试的理想分布,其中端到端(E2E)测试位于金字塔顶端,用于验证整个系统的功能。NestJS 提供了强大的测试工具,通过 Test.createTestingModule() 可以轻松创建测试模块,结合 supertest 等工具实现完整的 E2E 测试。本文将深入探讨 NestJS 中的测试金字塔,以及如何使用这些工具进行有效的 E2E 测试。

1. 测试金字塔基础概念

1.1 什么是测试金字塔?

测试金字塔是软件测试的一种模型,描述了不同层次测试的数量分布:

typescript
// 测试金字塔结构
// 1. 单元测试 (Unit Tests) - 70%
//    - 测试单个函数或类
//    - 运行速度快
//    - 成本低

// 2. 集成测试 (Integration Tests) - 20%
//    - 测试模块间交互
//    - 中等运行速度
//    - 中等成本

// 3. 端到端测试 (E2E Tests) - 10%
//    - 测试完整业务流程
//    - 运行速度慢
//    - 成本高

1.2 NestJS 测试工具

NestJS 提供了完整的测试工具链:

typescript
// NestJS 测试工具
// 1. @nestjs/testing - 核心测试模块
// 2. Test.createTestingModule() - 创建测试模块
// 3. 各种测试装饰器和工具函数
// 4. 与 Jest、Jasmine 等测试框架集成

2. 单元测试实现

2.1 服务层单元测试

typescript
// user.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
import { UserRepository } from './user.repository';

describe('UserService', () => {
  let service: UserService;
  let repository: MockUserRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: UserRepository,
          useClass: MockUserRepository,
        },
      ],
    }).compile();

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

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

  describe('createUser', () => {
    it('should create a new user', async () => {
      const createUserDto = { name: 'John', email: 'john@example.com' };
      const createdUser = { id: '1', ...createUserDto };
      
      jest.spyOn(repository, 'create').mockResolvedValue(createdUser);
      
      const result = await service.createUser(createUserDto);
      
      expect(result).toEqual(createdUser);
      expect(repository.create).toHaveBeenCalledWith(createUserDto);
    });

    it('should throw ConflictException when email already exists', async () => {
      const createUserDto = { name: 'John', email: 'john@example.com' };
      
      jest.spyOn(repository, 'findByEmail').mockResolvedValue({} as any);
      
      await expect(service.createUser(createUserDto))
        .rejects
        .toThrow(ConflictException);
    });
  });
});

// MockUserRepository
class MockUserRepository {
  async create(dto: any) {
    return Promise.resolve({ id: '1', ...dto });
  }
  
  async findByEmail(email: string) {
    return Promise.resolve(null);
  }
  
  async findById(id: string) {
    return Promise.resolve({ id, name: 'John', email: 'john@example.com' });
  }
}

2.2 控制器层单元测试

typescript
// user.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
import { UserService } from './user.service';

describe('UserController', () => {
  let controller: UserController;
  let service: MockUserService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [UserController],
      providers: [
        {
          provide: UserService,
          useClass: MockUserService,
        },
      ],
    }).compile();

    controller = module.get<UserController>(UserController);
    service = module.get(UserService);
  });

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

  describe('create', () => {
    it('should create a user', async () => {
      const createUserDto = { name: 'John', email: 'john@example.com' };
      const createdUser = { id: '1', ...createUserDto };
      
      jest.spyOn(service, 'createUser').mockResolvedValue(createdUser);
      
      const result = await controller.create(createUserDto);
      
      expect(result).toEqual(createdUser);
      expect(service.createUser).toHaveBeenCalledWith(createUserDto);
    });
  });

  describe('findOne', () => {
    it('should return a user', async () => {
      const userId = '1';
      const user = { id: userId, name: 'John', email: 'john@example.com' };
      
      jest.spyOn(service, 'getUserById').mockResolvedValue(user);
      
      const result = await controller.findOne(userId);
      
      expect(result).toEqual(user);
      expect(service.getUserById).toHaveBeenCalledWith(userId);
    });
  });
});

3. 集成测试实现

3.1 模块间集成测试

typescript
// user-order.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UserModule } from '../user/user.module';
import { OrderModule } from '../order/order.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from '../user/user.controller';
import { OrderController } from '../order/order.controller';

describe('User and Order Integration', () => {
  let userController: UserController;
  let orderController: OrderController;
  let app: INestApplication;

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        // 使用真实的模块而不是模拟模块
        UserModule,
        OrderModule,
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          entities: [User, Order],
          synchronize: true,
        }),
      ],
    }).compile();

    app = module.createNestApplication();
    await app.init();

    userController = app.get(UserController);
    orderController = app.get(OrderController);
  });

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

  it('should create user and place order', async () => {
    // 1. 创建用户
    const createUserDto = { name: 'John', email: 'john@example.com' };
    const user = await userController.create(createUserDto);
    
    expect(user).toBeDefined();
    expect(user.id).toBeDefined();
    
    // 2. 为用户创建订单
    const createOrderDto = { 
      userId: user.id, 
      items: [{ productId: '1', quantity: 2, price: 10 }] 
    };
    const order = await orderController.create(createOrderDto);
    
    expect(order).toBeDefined();
    expect(order.userId).toBe(user.id);
    expect(order.total).toBe(20);
  });
});

3.2 数据库集成测试

typescript
// database.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../user/user.entity';
import { UserService } from '../user/user.service';
import { UserRepository } from '../user/user.repository';

describe('Database Integration', () => {
  let service: UserService;
  let repository: UserRepository;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:', // 内存数据库用于测试
          entities: [User],
          synchronize: true, // 自动创建表结构
        }),
        TypeOrmModule.forFeature([User]),
      ],
      providers: [UserService, UserRepository],
    }).compile();

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

  it('should save and retrieve user from database', async () => {
    const createUserDto = { name: 'John', email: 'john@example.com' };
    
    // 保存用户
    const savedUser = await service.createUser(createUserDto);
    expect(savedUser.id).toBeDefined();
    
    // 从数据库检索用户
    const retrievedUser = await repository.findById(savedUser.id);
    expect(retrievedUser).toEqual(savedUser);
  });

  it('should find user by email', async () => {
    const createUserDto = { name: 'John', email: 'john@example.com' };
    
    // 创建用户
    await service.createUser(createUserDto);
    
    // 按邮箱查找用户
    const foundUser = await repository.findByEmail('john@example.com');
    expect(foundUser).toBeDefined();
    expect(foundUser.email).toBe('john@example.com');
  });
});

4. E2E 测试实现

4.1 基础 E2E 测试设置

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

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

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

4.2 完整的 E2E 测试示例

typescript
// user.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';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../src/user/user.entity';

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

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
        AppModule,
        TypeOrmModule.forRoot({
          type: 'sqlite',
          database: ':memory:',
          entities: [User],
          synchronize: true,
        }),
      ],
    }).compile();

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

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

  describe('/users (POST)', () => {
    it('should create a user', () => {
      const createUserDto = {
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123',
      };

      return request(app.getHttpServer())
        .post('/users')
        .send(createUserDto)
        .expect(201)
        .expect((res) => {
          expect(res.body).toMatchObject({
            name: 'John Doe',
            email: 'john@example.com',
          });
          expect(res.body.id).toBeDefined();
          expect(res.body.password).toBeUndefined(); // 密码不应该返回
        });
    });

    it('should reject invalid email', () => {
      const createUserDto = {
        name: 'John Doe',
        email: 'invalid-email',
        password: 'password123',
      };

      return request(app.getHttpServer())
        .post('/users')
        .send(createUserDto)
        .expect(400);
    });
  });

  describe('/users/:id (GET)', () => {
    let userId: string;

    beforeEach(async () => {
      // 先创建一个用户用于测试
      const response = await request(app.getHttpServer())
        .post('/users')
        .send({
          name: 'Jane Doe',
          email: 'jane@example.com',
          password: 'password123',
        })
        .expect(201);

      userId = response.body.id;
    });

    it('should get user by id', () => {
      return request(app.getHttpServer())
        .get(`/users/${userId}`)
        .expect(200)
        .expect((res) => {
          expect(res.body).toMatchObject({
            id: userId,
            name: 'Jane Doe',
            email: 'jane@example.com',
          });
        });
    });

    it('should return 404 for non-existent user', () => {
      return request(app.getHttpServer())
        .get('/users/99999')
        .expect(404);
    });
  });

  describe('/users (GET)', () => {
    it('should get all users', () => {
      return request(app.getHttpServer())
        .get('/users')
        .expect(200)
        .expect((res) => {
          expect(Array.isArray(res.body)).toBe(true);
        });
    });
  });
});

4.3 复杂业务流程 E2E 测试

typescript
// order.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('Order Flow (e2e)', () => {
  let app: INestApplication;
  let authToken: string;
  let userId: string;

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

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

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

  it('should complete full order flow', async () => {
    // 1. 用户注册
    const registerResponse = await request(app.getHttpServer())
      .post('/auth/register')
      .send({
        name: 'John Doe',
        email: 'john@example.com',
        password: 'password123',
      })
      .expect(201);

    userId = registerResponse.body.id;

    // 2. 用户登录
    const loginResponse = await request(app.getHttpServer())
      .post('/auth/login')
      .send({
        email: 'john@example.com',
        password: 'password123',
      })
      .expect(200);

    authToken = loginResponse.body.access_token;

    // 3. 创建订单
    const createOrderResponse = await request(app.getHttpServer())
      .post('/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .send({
        items: [
          { productId: '1', quantity: 2, price: 10 },
          { productId: '2', quantity: 1, price: 15 },
        ],
      })
      .expect(201);

    const orderId = createOrderResponse.body.id;
    expect(createOrderResponse.body.total).toBe(35);
    expect(createOrderResponse.body.status).toBe('pending');

    // 4. 获取订单详情
    const getOrderResponse = await request(app.getHttpServer())
      .get(`/orders/${orderId}`)
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(getOrderResponse.body.id).toBe(orderId);
    expect(getOrderResponse.body.userId).toBe(userId);

    // 5. 获取用户所有订单
    const getUserOrdersResponse = await request(app.getHttpServer())
      .get('/orders')
      .set('Authorization', `Bearer ${authToken}`)
      .expect(200);

    expect(Array.isArray(getUserOrdersResponse.body)).toBe(true);
    expect(getUserOrdersResponse.body.length).toBeGreaterThan(0);
  });
});

5. 测试工具和最佳实践

5.1 测试数据管理

typescript
// test/fixtures/user.fixture.ts
export class UserFixture {
  static createUserData(overrides: Partial<CreateUserDto> = {}): CreateUserDto {
    return {
      name: 'John Doe',
      email: 'john@example.com',
      password: 'password123',
      ...overrides,
    };
  }

  static createAdminUserData(): CreateUserDto {
    return {
      name: 'Admin User',
      email: 'admin@example.com',
      password: 'admin123',
      role: 'admin',
    };
  }
}

// test/factories/user.factory.ts
export class UserFactory {
  static async create(app: INestApplication, userData?: Partial<CreateUserDto>) {
    const createUserDto = UserFixture.createUserData(userData);
    
    const response = await request(app.getHttpServer())
      .post('/users')
      .send(createUserDto)
      .expect(201);
    
    return response.body;
  }

  static async createMany(app: INestApplication, count: number) {
    const users = [];
    for (let i = 0; i < count; i++) {
      const user = await this.create(app, {
        email: `user${i}@example.com`,
        name: `User ${i}`,
      });
      users.push(user);
    }
    return users;
  }
}

5.2 测试环境配置

typescript
// test/jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "collectCoverageFrom": [
    "src/**/*.(t|j)s"
  ],
  "coverageDirectory": "../coverage",
  "testTimeout": 30000
}

// package.json scripts
{
  "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:e2e": "jest --config ./test/jest-e2e.json",
    "test:e2e:watch": "jest --config ./test/jest-e2e.json --watch"
  }
}

5.3 测试数据库设置

typescript
// test/setup.ts
import { TypeOrmModule } from '@nestjs/typeorm';

export const testDatabaseOptions = {
  type: 'sqlite',
  database: ':memory:',
  entities: ['src/**/*.entity{.ts,.js}'],
  synchronize: true,
  dropSchema: true,
  logging: false,
};

// 在测试中使用
beforeEach(async () => {
  const module: TestingModule = await Test.createTestingModule({
    imports: [
      AppModule,
      TypeOrmModule.forRoot(testDatabaseOptions),
    ],
  }).compile();

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

6. 总结

NestJS 测试金字塔的实现:

  1. 单元测试:测试单个类或函数,使用模拟对象隔离依赖
  2. 集成测试:测试模块间交互,使用真实依赖但可能使用内存数据库
  3. E2E 测试:测试完整业务流程,启动完整应用进行端到端验证

测试工具的核心功能:

  1. Test.createTestingModule():创建测试模块,支持依赖注入
  2. supertest:HTTP 测试工具,模拟客户端请求
  3. 内存数据库:快速测试,避免外部依赖
  4. 测试装饰器:提供测试专用的依赖注入功能

测试最佳实践:

  1. 测试分层:按照测试金字塔合理分配测试比例
  2. 数据隔离:每个测试用例使用独立的测试数据
  3. 环境配置:为不同测试类型配置合适的环境
  4. 测试工具:使用专门的测试工具提高效率
  5. 持续集成:将测试集成到 CI/CD 流程中

通过合理应用 NestJS 的测试工具和遵循测试最佳实践,我们可以:

  1. 提高代码质量:通过测试发现和修复问题
  2. 增强系统稳定性:确保功能变更不会破坏现有功能
  3. 加快开发速度:通过自动化测试减少手动测试时间
  4. 改善代码设计:可测试的代码通常设计更好
  5. 降低维护成本:通过测试确保重构安全

至此,我们已经完成了 NestJS 专栏的所有内容,从基础概念到高级特性,从架构设计到最佳实践,全面覆盖了 NestJS 的核心知识点。