NestJS E2E测试 #
E2E测试概述 #
端到端(E2E)测试用于测试整个应用的工作流程,从HTTP请求到数据库操作,验证系统的集成是否正确。
安装依赖 #
bash
npm install --save-dev supertest @types/supertest
配置E2E测试 #
jest-e2e.json #
json
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}
基本E2E测试 #
创建测试文件 #
typescript
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!');
});
});
测试CRUD接口 #
用户接口测试 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
let accessToken: string;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('Authentication', () => {
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');
});
});
it('should fail login with invalid credentials', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({
email: 'test@example.com',
password: 'wrongpassword',
})
.expect(401);
});
});
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);
});
});
});
测试验证 #
验证错误测试 #
typescript
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);
});
});
测试分页 #
typescript
describe('Pagination', () => {
it('should return paginated results', () => {
return request(app.getHttpServer())
.get('/users?page=1&limit=10')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body).toHaveProperty('data');
expect(res.body).toHaveProperty('total');
expect(res.body).toHaveProperty('page', 1);
expect(res.body).toHaveProperty('limit', 10);
});
});
it('should use default pagination values', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body.page).toBe(1);
expect(res.body.limit).toBe(10);
});
});
});
测试文件上传 #
typescript
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);
});
});
测试数据库事务 #
使用测试数据库 #
typescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import * as request from 'supertest';
describe('UsersController (e2e)', () => {
let app: INestApplication;
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();
});
afterAll(async () => {
await app.close();
});
});
测试清理 #
每次测试后清理数据 #
typescript
describe('UsersController (e2e)', () => {
let app: INestApplication;
let dataSource: DataSource;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
dataSource = moduleFixture.get(DataSource);
await app.init();
});
afterAll(async () => {
await app.close();
});
afterEach(async () => {
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const repository = dataSource.getRepository(entity.name);
await repository.query(`DELETE FROM ${entity.tableName}`);
}
});
});
测试工具函数 #
创建测试用户 #
typescript
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,
};
}
测试辅助函数 #
typescript
function authRequest(
app: INestApplication,
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
url: string,
token: string,
) {
return request(app.getHttpServer())[method](url).set(
'Authorization',
`Bearer ${token}`,
);
}
运行E2E测试 #
bash
npm run test:e2e
CI/CD集成 #
GitHub Actions #
yaml
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: '18'
- run: npm ci
- run: npm run test:e2e
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
最佳实践 #
1. 独立测试 #
每个测试应该独立运行,不依赖其他测试的结果。
2. 清理数据 #
每次测试后清理数据,确保测试环境干净。
3. 使用环境变量 #
typescript
beforeAll(async () => {
process.env.NODE_ENV = 'test';
// ...
});
4. 并行测试 #
typescript
import { Test } from '@nestjs/testing';
describe.concurrent('UsersController (e2e)', () => {
// 并行执行测试
});
总结 #
本章学习了NestJS E2E测试:
- E2E测试配置
- CRUD接口测试
- 验证测试
- 分页测试
- 文件上传测试
- 数据库测试
- CI/CD集成
接下来,让我们学习 部署上线。
最后更新:2026-03-28