Mock Multer.single() Jest/Typescript



试图模拟我的"FileService"。

FileService:

class FileService() {
public uploadToS3SingleFile = (bucketName: string, formDataKey: string) =>
multer({
storage: multerS3({
s3: awsClientS3,
bucket: bucketName,
metadata: (req, file, cb) => {
cb(null, { fieldName: file.fieldName });
},
key: (req, _file, cb) => {
const uniqueFileName = `${Date.now()}--${req.params.name}`;
req.fileName = uniqueFileName;
cb(null, uniqueFileName);
},
}),
fileFilter: this.fileFilter,
}).single(formDataKey);
}

无法模拟multer.single(),已尝试:

const fileServiceMock = new (<new () => FileService>FileService)() as jest.Mocked<FileService>;
fileServiceMock.uploadToS3SingleFile = jest.fn().mockResolvedValueOnce(
(req: Request, res: Response, next: NextFunction) => {
next();
});

所以问题是如何用笑话和Typescript模拟上传到S3SingleFile?同样得到这个错误:";TypeError:this.fileService.uploadS3SingleFile(…)不是函数";

从开始,您想要创建FileService类的手动mock。

因此,首先让我们声明我们的简化源文件如下。

// file_service.ts
import multer from 'multer';
import multerS3 from 'multer-s3';
import { S3Client } from '@aws-sdk/client-s3';
export class FileService {
public uploadToS3SingleFile = (bucketName: string, formDataKey: string) =>
multer({
storage: multerS3({
s3: new S3Client({}), // ignored for simplification
bucket: bucketName,
metadata: (req, file, cb) => {
// Do something
},
key: (req, _file, cb) => {
// Do something else
},
}),
}).single(formDataKey);
}

您当前的方法是尝试创建一个mock,只使用类型中的实际服务,然后单独覆盖每个方法。这可以通过创建和导出mock服务来完成,但这并不是开玩笑的规范方法,但我在方法B中演示了这一点,因为它有自己的用例。


进近A

模拟任何模块(也称为文件导出)的最佳方法是简单地在要模拟的文件附近的__mocks__目录中创建一个模拟复制副本。然后在要模拟的测试中调用jest.mock('./path/to/module')

因此,在您的案例中,您将创建一个名为__mocks__/file_service.ts的文件来存储mock实现。

// __mocks__/file_service.ts
const uploadToS3SingleFileMock = jest.fn().mockReturnValue('testing'); // use the same mock for all instances, this could be a problem with parallel tests
export class FileService {
constructor () {
console.log('Using FileService from __mocks__/'); // just to show when using this mock or not
}
public uploadToS3SingleFile = uploadToS3SingleFileMock;
}

然后,要使用这个模拟实现,只需在测试中调用jest.mock

请注意,通过调用jest.mock,jest将首先查找工厂,然后在相应的__mocks__目录中查找命名模块。如果它什么也没发现,它将尝试自动模拟模块(如果启用的话)。

这种方法的强大之处在于,您可以模拟直接或间接调用的东西。请参阅以下示例。

直接

// fs_consumer_direct.ts
import { FileService } from './file_service';
export const directConsumer = (fs: FileService) => {
return fs.uploadToS3SingleFile('bucketName', 'formDataKey');
};

注意消费者希望服务直接通过

然后你会像这样测试这个消费者…

// fs_consumer_direct.test.ts
import { FileService } from './file_service'; // actually imports from __mocks__/
import { directConsumer } from './fs_consumer_direct';
// this makes mocks the imports of FileService for all imports above
jest.mock('./file_service');
it('should call uploadToS3SingleFile mock', () => {
const fsMock = new FileService();
const result = directConsumer(fsMock); // calls fsMock.uploadToS3SingleFile internally
expect(result).toBe('testing'); // this is for demo sake but you should test using correct types and functionality
expect(fsMock.uploadToS3SingleFile).toBeCalledTimes(1);
});

间接地

// fs_consumer_indirect.ts
import { FileService } from './file_service';
export const indirectConsumer = () => {
const fs = new FileService();
return fs.uploadToS3SingleFile('bucketName', 'formDataKey');
};

请注意,消费者导入服务,并且不希望在使用时直接传递该服务

然后你会像这样测试这个消费者…

// fs_consumer_indirect.test.ts
import { FileService } from './file_service'; // actually imports from __mocks__/
import { indirectConsumer } from './fs_consumer_indirect';
// this makes mocks the imports from file_service for all imports above
jest.mock('./file_service');
it('should call uploadToS3SingleFile mock', () => {
const result = indirectConsumer(); // note we are not passing the mock here
expect(result).toBe('testing'); // this is for demo sake but you should test using correct types and functionality
const fsMock = new FileService();
expect(fsMock.uploadToS3SingleFile).toBeCalledTimes(1);
});

注意,在这两种方法中,我们都像往常一样从file_service.ts文件导入FileService,但无论何时为模块调用jest.mock,都会返回mock模块。即使使用mock,也可以使用jest.requireActual来获得原始的非模拟模块,请参阅GitHub上的file_service.test.ts示例。

使用jest.mock工厂

jest.mock一起仅使用moduleName或路径,将查找__mocks__/或自动锁定模块。但是,您可以将第二个参数传递给jest.mock,这是一个factory函数,用于动态生成mock。无论何时传递factory,jest都将忽略__mocks__/目录中的所有mock。

直接与工厂联系

// fs_consumer_direct_factory.test.ts
import { FileService } from './file_service'; // actually imports module mock from factory below
import { directConsumer } from './fs_consumer_direct';
const uploadToS3SingleFileMock = jest.fn().mockReturnValue('local-mock');
// this makes mocks the imports from file_service for all imports above
jest.mock('./file_service', () => ({
FileService: jest.fn().mockImplementation(() => ({ // required for classes, see https://github.com/facebook/jest/issues/5023#issuecomment-355151664
uploadToS3SingleFile: uploadToS3SingleFileMock,
})),
}));
it('should call uploadToS3SingleFile mock', () => {
const fsMock = new FileService();
const result = directConsumer(fsMock); // calls fsMock.uploadToS3SingleFile internally
expect(result).toBe('local-mock'); // this is for demo sake but you should test using correct types and functionality
expect(fsMock.uploadToS3SingleFile).toBeCalledTimes(1);
expect(uploadToS3SingleFileMock).toBeCalledTimes(1); // equivalent to the line above
});

与工厂间接

// fs_consumer_indirect_factory.test.ts
import { indirectConsumer } from './fs_consumer_indirect';
const uploadToS3SingleFileMock = jest.fn().mockReturnValue('local-mock');
// this makes mocks the imports from file_service for all imports above
jest.mock('./file_service', () => ({
FileService: jest.fn().mockImplementation(() => ({ // required for classes, see https://github.com/facebook/jest/issues/5023#issuecomment-355151664
uploadToS3SingleFile: uploadToS3SingleFileMock,
})),
}));
it('should call uploadToS3SingleFile mock', () => {
const result = indirectConsumer(); // note we are not passing the mock here
expect(result).toBe('local-mock'); // this is for demo sake but you should test using correct types and functionality
expect(uploadToS3SingleFileMock).toBeCalledTimes(1);
});

这里有一个例子,可以通过调用jest.mock来模拟core_start.ts模块,并且这个测试文件中任何使用createCoreStartMock的导入模块现在都将使用这个模拟实现。

请在GitHub 上查看此方法的源文件


进近B

尽管通常不是理想的方法,但您所追求的方法仍然存在,通常用作函数并存储在源文件附近的文件中,文件名后缀为.mock.ts,或者全部集中到mocks.ts文件中。

您可以尝试使用jest.createMockFromModule来自动生成mock,但它并不总是能很好地处理mock类。

// file_service.mock.ts
import { RequestHandler } from 'express';
import { FileService } from './file_service';
export const createFileServiceMock = (options?: { name?: string }): jest.Mocked<FileService> => {
// jest attempts to create an automock of the modules exports, not great for classes
const FileServiceMock = jest.createMockFromModule<{ FileService: new() => FileService }>(
'./file_service.ts',
).FileService;
const fullMockService = new FileServiceMock();
return {
...fullMockService,
uploadToS3SingleFile: jest.fn<RequestHandler, [bucketName: string, formDataKey: string]>((bucketName, formDataKey) => {
// add custom mock behaviors here
console.log(bucketName, formDataKey);
return { mock: true, mockName: options?.name } as any;
}),
} as jest.Mocked<FileService>;
};

这种方法的好处在于,您可以传递选项来扩展对实现的控制,就像上面对mockName: options?.name所做的那样。

然后你会在任何测试文件中使用这个mock,比如…

// some_other_test.test.ts
import { FileService } from './file_service';
import { createFileServiceMock } from './file_service.mock';
const fakeConsumer = (fs: FileService) => {
return fs.uploadToS3SingleFile('bucketName', 'formDataKey');
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should mock FileService with default', () => {
const fsMock = createFileServiceMock();
const result = fakeConsumer(fsMock);
expect(fsMock.uploadToS3SingleFile).toBeCalledTimes(1);
expect(result).toMatchObject({ mock: true });
});
it('should mock FileService', () => {
const fsMock = createFileServiceMock({ name: 'testing' });
const result = fakeConsumer(fsMock);
expect(fsMock.uploadToS3SingleFile).toBeCalledTimes(1);
expect(result).toMatchObject({ mock: true, mockName: 'testing' });
});

在这个例子中,我假设我们只是将这个FileServicemock传递给内部使用它的东西。使用mock来测试自己是没有意义的。

如果要测试FileService本身,可以模拟multer模块以更好地促进测试。请参阅file_service.test.ts中的此示例。

这方面的一个例子是,createCoreStartMock可以从任何测试中导入和调用,并生成服务的完整模拟实现,允许使用重写实现模拟任何方法。

请在GitHub 上查看此方法的源文件


关于上述示例的一些一般注意事项

在大多数示例中,我只是用任意值(即{ mock: true })模拟返回。理想情况下,您在一定程度上遵循代码的类型和功能。还有很多为某些npm应用程序(如@jest-mock/express)定制的mock。

在模拟和测试中保持类型的准确性可能会有点困难,但在模拟测试中的功能时,类型就不那么重要了,所以至少要提供您需要或期望的内容,然后再添加更多内容来满足您的需求。any型有时是必要的,但不要把它当作拐杖。

为什么不使用codesandbox.io

我希望,我一开始尝试使用它,但后来遇到了#513(例如jest.mock is not a function🫠)并被迫使用GitHub。但幸运的是,您仍然可以在这里创建repo的GitHub代码空间,并从集成终端运行yarn test来直接测试代码。

其他良好资源

  • https://jestjs.io/docs/next/es6-class-mocks
  • https://stackoverflow.com/a/47430057/6943587
  • https://codewithhugo.com/express-request-response-mocking/expressjs的模型

最新更新