typescriptjestjsmulterts-jestmulter-s3

Mock Multer.single() Jest/Typescript


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"


Solution

  • 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.


    Approach A

    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.

    Directly

    // 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);
    });
    

    Indirectly

    // 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 the file_service.ts file like normal but whenever you call jest.mock for a module, the mock module will be returned instead. You can use jest.requireActual to get the original non-mocked module even when using mock, see this in file_service.test.ts example on GitHub.

    Using jest.mock factory

    Using 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.

    Direct with factory

    // 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
    });
    

    Indirect with factory

    // 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


    Approach B

    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


    Some general notes about the example above

    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.

    Why not use codesandbox.io?

    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.

    Other good resources