如何用笑话模拟 S3?

Posted

技术标签:

【中文标题】如何用笑话模拟 S3?【英文标题】:How to mock S3 with jest? 【发布时间】:2020-09-01 22:50:31 【问题描述】:

我正在尝试编写上传测试代码。 但我并没有低估如何正确使用jest.mock('aws-sdk')

export class S3Service 
  private readonly s3: S3;
  private readonly bucket: string;
  constructor(private readonly configService: ConfigService) 
    this.s3 = new S3(
      accessKeyId: this.configService.get(''),
      secretAccessKey: this.configService.get(''),
      region: this.configService.get(''),
    );
    this.bucket = this.configService.get('');
  
async upload(name: string, contentType: string, buffer: Buffer): Promise<string> 
    const upload = await this.s3.upload(params...).promise();
    return upload;
  

【问题讨论】:

【参考方案1】:

这里是单元测试解决方案:

s3Service.ts:

import  S3  from 'aws-sdk';

export class S3Service 
  private readonly s3: S3;
  private readonly bucket: string;
  constructor(private readonly configService) 
    this.s3 = new S3(
      accessKeyId: this.configService.get(''),
      secretAccessKey: this.configService.get(''),
      region: this.configService.get(''),
    );
    this.bucket = this.configService.get('');
  
  public async upload(name: string, contentType: string, buffer: Buffer): Promise<any> 
    const bucket = this.bucket;
    const params =  Bucket: bucket, Key: 'key', Body: buffer ;
    const upload = await this.s3.upload(params).promise();
    return upload;
  

s3Service.test.ts:

import  S3Service  from './s3Service';

const mS3Instance = 
  upload: jest.fn().mockReturnThis(),
  promise: jest.fn(),
;

jest.mock('aws-sdk', () => 
  return  S3: jest.fn(() => mS3Instance) ;
);

describe('61830632', () => 
  it('should upload correctly', async () => 
    const configService = 
      get: jest
        .fn()
        .mockReturnValueOnce('accessKeyId')
        .mockReturnValueOnce('secretAccessKey')
        .mockReturnValueOnce('us-east')
        .mockReturnValueOnce('bucket-dev'),
    ;
    mS3Instance.promise.mockResolvedValueOnce('fake response');
    const s3Service = new S3Service(configService);
    const actual = await s3Service.upload('name', 'contentType', Buffer.from('ok'));
    expect(actual).toEqual('fake response');
    expect(mS3Instance.upload).toBeCalledWith( Bucket: 'bucket-dev', Key: 'key', Body: Buffer.from('ok') );
  );
);

100% 覆盖率的单元测试结果:

 PASS  ***/61830632/s3Service.test.ts (11.362s)
  61830632
    ✓ should upload correctly (6ms)

--------------|---------|----------|---------|---------|-------------------
File          | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------|---------|----------|---------|---------|-------------------
All files     |     100 |      100 |     100 |     100 |                   
 s3Service.ts |     100 |      100 |     100 |     100 |                   
--------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        12.738s

【讨论】:

感谢您的说明性回答。从概念上讲,jest.fn().mockReturnThis() 做了什么?它可以让你链接调用吗? @cischa 是的。它可以让你链接调用。 @slideshowp2 在我的测试中复制您的代码会从开玩笑返回此错误:ReferenceError: .../__tests__/user_handler.spec.ts: jest.mock() 的模块工厂不允许引用任何输出范围变量。无效的变量访问:mS3Instance 允许的对象:Array, ArrayBuffer, Atomics, BigInt, BigInt64Array, BigUint64Array, Boolean, Buffer, DTRACE_HTTP_CLIENT_REQUEST, DTRACE_HTTP_CLIENT_RESPONSE, DTRACE_HTTP_SERVER_REQUEST, DTRACE_HTTP_SERVER_RESPONSE, DTRACE_NET_SERVER_CONNECTION, DTRACE_NET_STREAM /跨度> @BenjaminHeinke 这只是一个警告。将 mS3Instance 重命名为以 mock 开头的名称 - 例如 mockS3【参考方案2】:

这是我的解决方案:

jest.mock('aws-sdk', () => 
    class mockS3 
        getSignedUrl(op, obj) 
            return 'url';
        
    
    return 
        ...jest.requireActual('aws-sdk'),
        S3: mockS3,
    ;
);

【讨论】:

【参考方案3】:

如果您使用的是 NestJS,它可能比您想象的要容易。

文件上传模块:

import  Module  from '@nestjs/common';
import  ConfigService  from '@nestjs/config';
import  TypeOrmModule  from '@nestjs/typeorm';
import  S3  from 'aws-sdk';
import  FileUploadController  from './file-upload.controller';
import  FileUploadRepository  from './file-upload.repository';
import  FileUploadService  from './file-upload.service';
@Module(
  imports: [TypeOrmModule.forFeature([FileUploadRepository])],
  controllers: [FileUploadController],
  providers: [
    FileUploadService,
    
      provide: S3,
      useFactory: (configService: ConfigService) =>
        new S3(
          accessKeyId: configService.get('AWS_ACCESS_KEY'),
          secretAccessKey: configService.get('AWS_ACCESS_SECRET'),
          region: configService.get('AWS_REGION'),
        ),
    ,
  ],
)
export class FileUploadModule 

服务本身:

import  Injectable, InternalServerErrorException  from '@nestjs/common';
import  ConfigService  from '@nestjs/config';
import  InjectRepository  from '@nestjs/typeorm';
import  S3  from 'aws-sdk';
import  v4 as uuid  from 'uuid';
import  FileUpload  from './file-upload.entity';
import  FileUploadRepository  from './file-upload.repository';

@Injectable()
export class FileUploadService 
  constructor(
    @InjectRepository(FileUploadRepository)
    private readonly publicFileRepository: FileUploadRepository,
    private readonly configService: ConfigService,
    private readonly s3: S3,
  ) 

  async uploadPublicFile(
    dataBuffer: Buffer,
    filename: string,
  ): Promise<FileUpload> 
    const uploadResult = await this.s3
      .upload(
        Bucket: this.configService.get('AWS_BUCKET_NAME'),
        Body: dataBuffer,
        Key: `$uuid()-$filename`,
      )
      .promise();

    const createdFile = this.publicFileRepository.create(
      key: uploadResult.Key,
      url: uploadResult.Location,
    );
    await this.publicFileRepository.save(createdFile);
    return createdFile;
  

  async deletePublicFile(
    publicFileId: string,
    publicFileKey: string,
  ): Promise<FileUpload> 
    const response = await this.s3
      .deleteObject(
        Bucket: this.configService.get('AWS_BUCKET_NAME'),
        Key: publicFileKey,
      )
      .promise();
    if (!response) 
      throw new InternalServerErrorException(
        `Could not delete file $publicFileKey`,
      );
    
    const  raw: deletedItem  = await this.publicFileRepository.delete(
      publicFileId,
    );
    return deletedItem;
  


最后是测试:

import  InternalServerErrorException  from '@nestjs/common';
import  ConfigService  from '@nestjs/config';
import  Test, TestingModule  from '@nestjs/testing';
import  S3  from 'aws-sdk';
import  mockFileUpload  from './file-upload.mock';
import  FileUploadRepository  from './file-upload.repository';
import  FileUploadService  from './file-upload.service';

export const mockFileUploadRepository = () => (
  create: jest.fn(),
  save: jest.fn(),
  delete: jest.fn(),
);

const mS3Instance = 
  upload: jest.fn().mockReturnThis(),
  promise: jest.fn(),
  deleteObject: jest.fn().mockReturnThis(),
;

describe('FileUploadService', () => 
  let service: FileUploadService;
  let repository;
  let s3Service;

  beforeEach(async () => 
    const module: TestingModule = await Test.createTestingModule(
      providers: [
        FileUploadService,
        
          provide: FileUploadRepository,
          useFactory: mockFileUploadRepository,
        ,
        
          provide: ConfigService,
          useValue: 
            get: jest.fn(),
          ,
        ,
        
          provide: S3,
          useFactory: () => mS3Instance,
        ,
      ],
    ).compile();

    service = module.get<FileUploadService>(FileUploadService);
    repository = module.get<FileUploadRepository>(FileUploadRepository);
    repository = module.get<FileUploadRepository>(FileUploadRepository);
    s3Service = module.get<S3>(S3);
  );

  describe('FileUploadService.uploadPublicFile', () => 
    it('should create public file and throw no error', async () => 
      repository.create.mockResolvedValue(mockFileUpload);
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue(
        Key: 'some-key',
        Location: 'some-location',
      );

      const file = Buffer.alloc(513, '0');
      const response = await service.uploadPublicFile(file, 'somefilename');
      expect(response).toBeDefined();
    );
  );

  describe('FileUploadService.deletePublicFile', () => 
    it('should delete public file and throw no error', async () => 
      repository.delete.mockResolvedValue( raw: mockFileUpload );
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue(
        Key: 'some-key',
        Location: 'some-location',
      );

      const response = await service.deletePublicFile('someid', 'somefilename');
      expect(response).toBeDefined();
    );

    it('should not delete public file and throw InternalServerErrorException', async () => 
      repository.delete.mockResolvedValue( raw: mockFileUpload );
      repository.save.mockResolvedValue(mockFileUpload);

      s3Service.promise = jest.fn().mockResolvedValue(null);

      const promise = service.deletePublicFile('someid', 'somefilename');
      expect(promise).rejects.toThrow(InternalServerErrorException);
    );
  );
);

【讨论】:

谢谢。我可以确认上述方法有效。

以上是关于如何用笑话模拟 S3?的主要内容,如果未能解决你的问题,请参考以下文章

笑话模拟工厂不适用于模拟类

笑话:模拟 RxJs 管道

笑话:如何正确模拟节点模块?

用笑话模拟 fs 函数

笑话:如何模拟监听事件的依赖项?

使用笑话模拟时的打字稿错误