前言
开发一个服务端项目,除了基本的页面渲染功能、提供 restful api 接口(提供增删查改接口)外,也离不开登陆注册功能、文件上传下载功能等等。本文试着使用 Nest 去开发一个文件上传功能,并探索了如何去实现上传文件的验证。
此外,为了方便地进行上传文件功能的测试,本文借助了Jest中的e2e测试,详细地介绍了 jest 测试框架的常用语法规则,以及如何去书写一个e2e测试文件。
简单的文件上传功能
Nest 内置模块中包含了可用于 Express 中的 multer 中间件,因此可以处理multipart/form-data
格式的数据,能用于上传文件需求。接下来实践一下。
首先,安装 multer 类型包:
pnpm i -D @types/multer
之后就可以使用类型声明Express.Multer.File
了。需要注意的是,multer 不会处理任何不是multipart/form-data
的表单,而且这个包不适用于FastifyAdapter
。
在app.controller.ts
文件中,定义一个 /file
路由,使用的是 POST 方法。我们需要使用@UseInterceptors
装饰器,引入拦截器 FileInterceptor()。
uploadFile 函数的传参通过 @Body() 装饰器声明,上传文件通过 @UploadedFile() 装饰器声明。在接收到参数后,返回值是 { body, file } 。
import {
Body,
Controller,
Get,
Post,
Param,
UploadedFile,
UseInterceptors,
ParseFilePipeBuilder,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';
import { AppService } from './app.service';
import { SampleDto } from './sample.dto';
@UseInterceptors(FileInterceptor('file'))
@Post('file')
uploadFile(
@Body() body: SampleDto,
@UploadedFile() file: Express.Multer.File,
) {
// console.log(file);
return {
body,
file: file.buffer.toString(),
};
}
数据验证
得到的只是简单的一个上传功能,如果我们要添加上传数据和上传文件的校验呢?
这里定义一个校验函数uploadFileAndPassValidation
。在@UploadedFile()
装饰器中,body 部分是 SampleDto 格式,可以借助class-validator
包校验格式,以及设置校验不通过时的提示内容;传参 ParseFilePipeBuilder 类的实例,比如限制 json文件(fileType: 'json')、上传文件是非必需的(fileIsRequired: false, 默认值是必需)。
类似地,定义校验失败的函数uploadFileAndFailValidation
,限制 jpg文件、上传文件是必需的。
@UseInterceptors(FileInterceptor('file'))
@Post('file/pass-validation')
uploadFileAndPassValidation(
@Body() body: SampleDto,
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'json',
})
.build({
fileIsRequired: false,
}),
)
file?: Express.Multer.File,
) {
return {
body,
file: file?.buffer.toString(),
};
}
@UseInterceptors(FileInterceptor('file'))
@Post('file/fail-validation')
uploadFileAndFailValidation(
@Body() body: SampleDto,
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'jpg',
})
.build(),
)
file: Express.Multer.File,
) {
return {
body,
file: file.buffer.toString(),
};
}
此外,数据验证需要安装文件校验相关的包:
pnpm i class-validator class-transformer
新建 dto 文件:
// src/sample.dto.ts
import { IsNotEmpty, Length } from 'class-validator';
export class SampleDto {
@IsNotEmpty({ message: '用户名不为空' })
@Length(1, 20, { message: 'name的长度不能小于1且不能大于20' })
name: string;
}
在main.ts中,通过 ValidationPipe 设置全局管道:
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
e2e测试
如何测试代码功能是否正常呢?我们可以新建一个前端表单页面,调用相应的接口(过程比较繁琐)。同时,还可以使用 postman 进行测试,不过在用于测试文件的上传功能时也显得有点繁琐了。此外,更方便、更高效的方法是,借助 Jest 中的 e2e测试
。
Jest 是一个专注于简便的 JavaScript 测试框架,可以在使用了 Babel、TS、node、Vue、React 的项目中使用。Jest 具有零配置、开箱即用、快照功能、并行测试、输出测试覆盖率等特点。
e2e测试(End-To-End Testing,端到端测试) 是一种从头到尾测试整个软件产品以确保应用程序流程按预期运行的技术。e2e测试的主要目的是通过模拟真实用户场景并验证被测系统及其组件的集成和数据完整性,主要从最终用户的体验进行测试。
首先,在 test 目录下新建upload.e2e-spec.ts
文件,也就是e2e测试的文件。在该文件中,describe 测试套件 中包含了一组测试流程,每个测试流程由 it 定义。
接着,在 beforeAll 钩子中定义了测试模块 moduleRef ,创建并初始化一个 Nest app;测试结束时在 afterAll 钩子中调用 close() 方法关闭 app。
文件内容如下所示:
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { readFileSync } from 'fs';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('E2E FileTest', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
await app.init();
});
it('should allow for file uploads', async () => {
return request(app.getHttpServer())
.post('/file')
.attach('file', './package.json')
.field('name', 'test')
.expect(201)
.expect({
body: {
name: 'test',
},
file: readFileSync('./package.json').toString(),
});
});
it('should allow for file uploads that pass validation', async () => {
return request(app.getHttpServer())
.post('/file/pass-validation')
.attach('file', './package.json')
.field('name', 'test')
.expect(201)
.expect({
body: {
name: 'test',
},
file: readFileSync('./package.json').toString(),
});
});
it('should throw for file uploads that do not pass validation', async () => {
return request(app.getHttpServer())
.post('/file/fail-validation')
.attach('file', './package.json')
.field('name', 'test')
.expect(400);
});
it('should throw when file is required but no file is uploaded', async () => {
return request(app.getHttpServer())
.post('/file/fail-validation')
.expect(400);
});
it('should allow for optional file uploads with validation enabled (fixes #10017)', () => {
return request(app.getHttpServer())
.post('/file/pass-validation')
.expect(201);
});
afterAll(async () => {
await app.close();
});
});
jest 语句的写法很明了,比如测试 /file 路由,首先是请求app的接口request(app.getHttpServer()).post('/file')
,参数是通过 field() 定义的,文件是通过 attach() 定义的,最后在 .expect() 中写上期待的返回值的内容和对应的 http 状态码即可。
接着,测试 /file/pass-validation 路由和 /file/fail-validation 路由。
最后,运行 e2e测试:pnpm test:e2e
,也就是调用了jest --config ./test/jest-e2e.json
,文件中指定了test目录下的所有 .ts(.e2e-spec.ts$) 测试文件都会运行。测试结果如下:
PASS test/app.e2e-spec.ts (10.833 s)
PASS test/upload.e2e-spec.ts (10.938 s)
Test Suites: 2 passed, 2 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 20.668 s
Ran all test suites.
至此,也就是e2e测试的整个流程。