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