如何在NestJS/TypeScript自动化测试中模拟MinIO服务?
在NestJS自动化测试中替代MinIO的内存方案
背景
我正在开发一款基于NestJS的应用,用MinIO替代Amazon S3存储文档,通过Docker Compose部署,包含NestJS后端(集成MinIO客户端)、PostgreSQL数据库和MinIO服务器。
在自动化测试中,我选择用内存型SQLite替代PostgreSQL,而非模拟数据库函数——这个方案实现更简单,也不容易出错。现在我想对MinIO采取类似方案:要么让MinIO在内存中运行,要么模拟它的操作,让测试不需要依赖外部MinIO服务器。
待测试的相关代码
CloudStorageService
import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { Client } from 'minio'; import * as path from 'path'; export type CloudStorageConfigs = { endPoint: string; bucketName: string; port: number; useSSL: boolean; accessKey: string; secretKey: string; }; @Injectable() export class CloudStorageService { private readonly client: Client; constructor(private readonly configs: CloudStorageConfigs) { this.client = new Client(configs); } async save(file: Express.Multer.File[], directory?: string) { await Promise.all(file.map((file) => this.saveSingleFile(file, directory))); return true; } private async saveSingleFile(file: Express.Multer.File, directory?: string) { const filename = this.getUniqueFilename(file); const objectName = path.join(directory, filename); const buffer = Buffer.from(file.buffer); if (!(await this.client.bucketExists(this.configs.bucketName))) { await this.client.makeBucket(this.configs.bucketName); } await this.client.putObject(this.configs.bucketName, objectName, buffer); return true; } async list(directory: string) { return await new Promise((resolve, reject) => { const objects: string[] = []; const objectsStream = this.client.listObjectsV2( this.configs.bucketName, directory, false, ); objectsStream.on('data', (obj) => { objects.push(obj.name || obj.prefix); }); objectsStream.on('error', (err) => { reject(err); }); objectsStream.on('end', () => { resolve(objects); }); }); } async delete(objects: string[]) { await this.client.removeObjects(this.configs.bucketName, objects); return true; } private getUniqueFilename(file: Express.Multer.File): string { if (!file?.originalname) { throw new UnprocessableEntityException('File not defined'); } const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const tokens = file.originalname.split('.'); let ext; if (!tokens || tokens.length < 2) { ext == ''; } else { ext = '.' + tokens[tokens.length - 1]; } const filename = `${uniqueSuffix}${ext}`; return filename; } }
CloudStorageModule
import { Module } from '@nestjs/common'; import { MulterModule } from '@nestjs/platform-express'; import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; import { CloudStorageConfigs, CloudStorageService, } from './services/file/cloud-storage.service'; @Module({ imports: [ MulterModule.register({ // dest: './uploads', }), ServeStaticModule.forRoot({ rootPath: join(process.cwd(), 'public') }), ], }) export class CloudStorageModule { static forRoot(storageConfigs: CloudStorageConfigs) { return { module: CloudStorageModule, providers: [ { provide: CloudStorageService, useValue: new CloudStorageService(storageConfigs), }, ], exports: [CloudStorageService], }; } }
ImageModule
import { Module } from '@nestjs/common'; import { CloudStorageModule } from '../cloud-storage/cloud-storage.module'; import { MinioCongifs } from '../cloud-storage/configs/minio/minio.congifs'; import { ImageController } from './controller/image.controller'; @Module({ imports: [ CloudStorageModule.forRoot({ endPoint: MinioCongifs.endpoint, port: MinioCongifs.port, useSSL: MinioCongifs.useSSL, accessKey: MinioCongifs.accessKey, secretKey: MinioCongifs.secretKey, bucketName: MinioCongifs.bucketName, }), ], controllers: [ImageController], providers: [], }) export class ImageModule {}
测试模块生成函数
export async function getTestingModule( additionalMetadata?: ModuleMetadata, ): Promise<TestingModule> { return await Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forFeature([ UserEntity ]), ConfigModule.forRoot({ isGlobal: true }), TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', autoLoadEntities: true, synchronize: true, }), PassportModule, JwtModule.register({ secret: JWTConfigs.ACCESS_TOKEN_SECRET, signOptions: { expiresIn: JWTConfigs.ACCESS_TOKEN_EXPIRATION }, }), ...(additionalMetadata?.imports || []), ], providers: [ AppService, AuthenticationService, EncryptionService, JwtStrategy, LocalStrategy, RefreshTokenRepository, TokenService, UserService, CloudStorageService, ...(additionalMetadata?.providers || []), { provide: APP_GUARD, useClass: JwtAuthenticationGuard }, { provide: APP_GUARD, useClass: RolesGuard }, ], controllers: [ AppController, AuthenticationController, UserController, ImageController, ...(additionalMetadata?.controllers || []), ], }).compile(); }
可行方案
针对需求,有两种可靠的实现方式:
1. 使用MinIO内存模式
MinIO本身支持内存存储模式,完全在内存中运行,无需持久化到磁盘,非常适合测试场景。
实现步骤:
- 在测试环境中启动内存版MinIO实例,可直接通过
minio包配置启动,或用命令行启动内存模式:minio server /data --address :9000 --memory - 修改测试模块中
CloudStorageService的配置,指向这个内存实例的地址、密钥等参数。
2. 模拟MinIO客户端(依赖注入替换)
直接模拟MinIO客户端的核心方法,无需启动任何外部服务,通过依赖注入替换真实服务。
实现步骤:
- 创建模拟的
CloudStorageService,用内存数据结构模拟存储逻辑:@Injectable() export class MockCloudStorageService { private storage = new Map<string, Buffer>(); private buckets = new Set<string>(); async save(file: Express.Multer.File[], directory?: string) { await Promise.all(file.map(file => this.saveSingleFile(file, directory))); return true; } private async saveSingleFile(file: Express.Multer.File, directory?: string) { const filename = this.getUniqueFilename(file); const objectName = path.join(directory || '', filename); const bucketName = 'test-bucket'; if (!this.buckets.has(bucketName)) { this.buckets.add(bucketName); } this.storage.set(`${bucketName}/${objectName}`, Buffer.from(file.buffer)); return true; } async list(directory: string) { const bucketName = 'test-bucket'; return Array.from(this.storage.keys()) .filter(key => key.startsWith(`${bucketName}/${directory}`)) .map(key => key.replace(`${bucketName}/`, '')); } async delete(objects: string[]) { const bucketName = 'test-bucket'; objects.forEach(obj => this.storage.delete(`${bucketName}/${obj}`)); return true; } private getUniqueFilename(file: Express.Multer.File): string { if (!file?.originalname) { throw new UnprocessableEntityException('File not defined'); } const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const tokens = file.originalname.split('.'); let ext = ''; if (tokens && tokens.length >= 2) { ext = '.' + tokens[tokens.length - 1]; } return `${uniqueSuffix}${ext}`; } } - 在测试模块中替换真实服务:
export async function getTestingModule( additionalMetadata?: ModuleMetadata, ): Promise<TestingModule> { return await Test.createTestingModule({ // ...其他配置 providers: [ // ...其他服务 { provide: CloudStorageService, useClass: MockCloudStorageService }, // ...其他配置 ], }).compile(); }
方案对比
- 内存MinIO:更贴近生产环境,能测试真实的MinIO客户端交互,适合集成测试。
- 模拟服务:轻量快速,完全无外部依赖,适合单元测试。
可根据测试类型选择对应的方案。
内容的提问来源于stack exchange,提问作者user3788133
相关产品推荐
相关产品推荐

