Trying to mock my '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);
}
Can't mock multer.single(), tried:
const fileServiceMock = new (<new () => FileService>FileService)() as jest.Mocked<FileService>;
fileServiceMock.uploadToS3SingleFile = jest.fn().mockResolvedValueOnce(
(req: Request, res: Response, next: NextFunction) => {
next();
});
So the question is how to mock uploadToS3SingleFile with jest and Typescript? Also getting this error: "TypeError: this.fileService.uploadS3SingleFile(...) is not a function"
From to sound of it you want to create a manual mock of the FileService
class.
So first of all let's just declare our simplified source file as below.
// 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);
}
Your current approach is attempting to create a mock with the actual service only in type then override each method individually. This can be done by just creating and exporting the mock service but this is not exactly the canonical approach in jest, but I demonstrate this in Approach B as it has its use cases.
The best way to mock any module (aka file exports) is to simply create a mock replica in a __mocks__
directory adjacent to the file you want to mock. Then call jest.mock('./path/to/module')
in the test you want to mock.
So in your case you would create a file called __mocks__/file_service.ts
to store the mock implementation.
// __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;
}
Then to use this mock implementation you would just call jest.mock
inside the test.
Note that by calling
jest.mock
jest will first look for a factory then in the respective__mocks__
directory for the named module. If it finds nothing it will attempt to auto mock the module, if enabled.
The power of this approach is that you can mock things that are call directly or indirectly. See examples below.
// fs_consumer_direct.ts
import { FileService } from './file_service';
export const directConsumer = (fs: FileService) => {
return fs.uploadToS3SingleFile('bucketName', 'formDataKey');
};
Notice the consumer expect the service to be passed directly
You would then test this consumer like so...
// 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');
};
Notice the consumer imports the service and does not expect it to be passed directly when used
You would then test this consumer like so...
// 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);
});
Notice in both approaches we import
FileService
from thefile_service.ts
file like normal but whenever you calljest.mock
for a module, the mock module will be returned instead. You can usejest.requireActual
to get the original non-mocked module even when usingmock
, see this infile_service.test.ts
example on GitHub.
jest.mock
factoryUsing jest.mock
with only the moduleName
or path, will look in __mocks__/
or automock the module. But you may pass a second argument to jest.mock
which is a factory
function to generate a mock on-the-fly. Whenever you pass a factory
jest will ignore all mocks in __mocks__/
directory.
// 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);
});
An example of this in the wild would be here where core_start.ts
module can be mocked by calling jest.mock
and any imported module in this test file that uses createCoreStartMock
will now use this mock implementation.
See source files for this approach on GitHub
Though usually not the ideal approach, the approach you where after is still seen in the wild, usually used as a function and stored in a file adjacent to the source file with the filename postfixed with .mock.ts
or all lumped into a mocks.ts
file.
You could attempt to use jest.createMockFromModule
to auto generate the mock, but it does not always do a great job with mocking classes.
// 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>;
};
The nice thing about this approach is that you can pass options to extend the control of the implementation as is done with the
mockName: options?.name
above.
Then you would use this mock in any test file like so...
// 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' });
});
In this example I am assuming we just pass this
FileService
mock to something that uses it internally. It wouldn't make sense to use the mock to test itself.
If you were to test the FileService
itself you could mock the multer
module to better facilitate testing. See this example at file_service.test.ts
.
An example of this in the wild would be here where createCoreStartMock
can be imported and called from any test and generates a full mock implementation of the service, allowing any method to be mocked with an override implementation.
See source files for this approach on GitHub
Throughout most of these examples I simply mock the returns with arbitrary values (i.e. { mock: true }
) ideally, you follow the types and functionality of the code to some degree. There are also plenty of mocks custom made for certain applications on npm like @jest-mock/express
.
Keeping type accuracy in mock and tests in general can prove to be a little challenging but types are less important when mocking functionality in tests, so at a minimum provide what you need or expect then add more to suit your needs along the way. The any
type is sometimes necessary but don't use it as a crutch.
I wish, I tried using it in the beginning but then ran into #513 (e.g. jest.mock is not a function
🫠) and was forced to use GitHub instead. But fortunately, you could still create a GitHub Codespace of the repo here and run yarn test
from the integrated terminal to test out the code directly.