E2E测试
本教程共 47 篇 · 第 45 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:掌握NestJS端到端测试,学会用Supertest模拟HTTP请求,测试完整的API流程,确保系统各组件协同工作。
单元测试测试单个类,E2E测试测试整个系统。从HTTP请求进去,走完控制器、服务、数据库,验证整个流程是否正确。就像真实用户在使用你的API。
安装依赖
E2E测试需要Supertest来模拟HTTP请求:
npm install --save-dev supertest @types/supertest
Nest CLI创建的项目已经自带E2E测试配置。看看test/jest-e2e.json:
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
testRegex匹配.e2e-spec.ts结尾的文件。
基本E2E测试
用Nest CLI创建的项目已经有个示例E2E测试,在test/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;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
跟单元测试的区别:
- 用
createNestApplication()创建完整的应用实例 - 用
app.init()初始化应用 - 用
request(app.getHttpServer())发送HTTP请求 - 测试结束后用
app.close()关闭应用
Note
beforeAll在所有测试前执行一次,afterAll在所有测试后执行。beforeEach和afterEach则在每个测试前后执行。
测试CRUD接口
假设有个用户管理API,测试完整的增删改查流程。
注册和登录
describe('Authentication', () => {
let accessToken: string;
it('should register a new user', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'Test User',
email: 'test@example.com',
password: 'password123',
})
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('access_token');
accessToken = res.body.access_token;
});
});
it('should login with valid credentials', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'password123',
})
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('access_token');
accessToken = res.body.access_token;
});
});
it('should fail login with invalid credentials', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
})
.expect(401);
});
});
send()发送请求体,expect()断言响应状态码或内容。
用户操作
describe('Users', () => {
it('should return all users', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});
it('should return a user by id', () => {
return request(app.getHttpServer())
.get('/users/1')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('id', 1);
});
});
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/99999')
.set('Authorization', `Bearer ${accessToken}`)
.expect(404);
});
it('should create a new user', () => {
return request(app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${accessToken}`)
.send({
name: 'New User',
email: 'newuser@example.com',
password: 'password123',
})
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('New User');
});
});
it('should update a user', () => {
return request(app.getHttpServer())
.patch('/users/1')
.set('Authorization', `Bearer ${accessToken}`)
.send({ name: 'Updated Name' })
.expect(200)
.expect((res) => {
expect(res.body.name).toBe('Updated Name');
});
});
it('should delete a user', () => {
return request(app.getHttpServer())
.delete('/users/1')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
});
});
set()设置请求头,用来传Authorization Token。
测试验证
验证管道会拒绝不合法的数据:
describe('Validation', () => {
it('should fail with invalid email', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'Test User',
email: 'invalid-email',
password: 'password123',
})
.expect(400);
});
it('should fail with short password', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'Test User',
email: 'test@example.com',
password: '123',
})
.expect(400);
});
it('should fail with missing required fields', () => {
return request(app.getHttpServer())
.post('/auth/register')
.send({ name: 'Test User' })
.expect(400);
});
});
验证失败返回400状态码。
覆盖提供者
E2E测试通常用真实的数据库和服务。但有时候想替换某些服务,比如不发真实的邮件:
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(EmailService)
.useValue({
sendEmail: jest.fn().mockResolvedValue(true),
})
.compile();
overrideProvider()跟单元测试里一样,可以替换任何提供者。
使用测试数据库
E2E测试最好用独立的测试数据库,别污染开发数据。
内存数据库
用SQLite内存数据库,测试完数据自动消失:
import { TypeOrmModule } from '@nestjs/typeorm';
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [__dirname + '/../src/**/*.entity{.ts,.js}'],
synchronize: true,
}),
AppModule,
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
synchronize: true自动根据实体创建表结构。
清理数据
每个测试后清理数据,保证测试独立:
import { DataSource } from 'typeorm';
describe('UsersController (e2e)', () => {
let app: INestApplication;
let dataSource: DataSource;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
dataSource = moduleFixture.get(DataSource);
await app.init();
});
afterEach(async () => {
// 清空所有表
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const repository = dataSource.getRepository(entity.name);
await repository.query(`DELETE FROM ${entity.tableName}`);
}
});
afterAll(async () => {
await app.close();
});
});
测试工具函数
测试多了,重复代码就多。提取工具函数简化测试。
创建测试用户
async function createTestUser(app: INestApplication) {
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
name: 'Test User',
email: `test${Date.now()}@example.com`,
password: 'password123',
});
return {
user: response.body.user,
accessToken: response.body.access_token,
};
}
使用时:
it('should do something', async () => {
const { accessToken } = await createTestUser(app);
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200);
});
认证请求辅助函数
function authRequest(
app: INestApplication,
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string,
token: string,
) {
return request(app.getHttpServer())[method](url)
.set('Authorization', `Bearer ${token}`);
}
使用时:
it('should return users', () => {
return authRequest(app, 'get', '/users', accessToken)
.expect(200);
});
测试文件上传
describe('File Upload', () => {
it('should upload a file', () => {
return request(app.getHttpServer())
.post('/upload')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', 'test/fixtures/sample.png')
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('filename');
expect(res.body).toHaveProperty('url');
});
});
it('should reject invalid file type', () => {
return request(app.getHttpServer())
.post('/upload')
.set('Authorization', `Bearer ${accessToken}`)
.attach('file', 'test/fixtures/sample.exe')
.expect(400);
});
});
attach()上传文件,第一个参数是字段名,第二个是文件路径。
运行E2E测试
npm run test:e2e
这个命令会读取test/jest-e2e.json配置,运行所有.e2e-spec.ts文件。
TipE2E测试比单元测试慢,因为要启动完整的应用、连接数据库。开发时可以只跑单元测试,提交代码前再跑E2E测试。
CI/CD集成
在CI环境跑E2E测试,需要配置测试数据库。以GitHub Actions为例:
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:14
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run test:e2e
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
启动PostgreSQL容器,设置环境变量,跑E2E测试。
最佳实践
测试要独立
每个测试不依赖其他测试的结果。测试A的失败不应该影响测试B。
清理数据
每次测试后清理数据,保证测试环境干净。不然测试之间会互相干扰。
用环境变量
beforeAll(async () => {
process.env.NODE_ENV = 'test';
// 根据环境加载不同配置
});
测试环境和开发环境用不同的数据库、不同的配置。
测试边界情况
不只测正常流程,还要测异常情况:
- 参数缺失
- 参数格式错误
- 权限不足
- 资源不存在
测试顺序
测试有依赖关系时,用describe分组,按顺序执行:
describe('User Flow', () => {
it('1. should register', () => {});
it('2. should login', () => {});
it('3. should get profile', () => {});
it('4. should update profile', () => {});
it('5. should logout', () => {});
});
WarningE2E测试不能覆盖所有场景。复杂的业务逻辑还是用单元测试。E2E测试主要验证API接口是否正确,各组件是否正确集成。
小结
这一章学了NestJS E2E测试:
基本结构:用createNestApplication()创建应用,supertest发送HTTP请求。
测试CRUD:测试注册、登录、增删改查等完整流程。
覆盖提供者:用overrideProvider()替换特定服务,比如不发真实邮件。
测试数据库:用SQLite内存数据库或独立的测试数据库,测试后清理数据。
工具函数:提取重复代码,简化测试。
文件上传:用attach()测试文件上传接口。
CI/CD集成:在CI环境配置测试数据库,自动跑E2E测试。
E2E测试是最后一道防线。单元测试保证每个零件没问题,E2E测试保证整台机器能正常运转。下一章我们聊部署上线。