'How do I completely mock an imported service class

I am looking to test my next API route which uses the micro framework (similar enough to express when using next-connect).

I have a service:

export class UserService {
  constructor(...) {}

  async findUser({ email }: Pick<IUserModel, 'email'>) {
    ...
  }
}

My API endpoint:

import { NextApiRequest, NextApiResponse } from 'next';
import nc from 'next-connect';
import { UserService } from './UserService'; // Mock and prevent execution

const userService = new UserService();

export default nc().post(async (req: NextApiRequest, res: NextApiResponse) => {
  try {
    userService.findUser({ email: '[email protected]' });
    return res.status(200).send({ done: true });
  } catch (error: any) {
    return res.status(500).end(error.message);
  }
});

I would like to completely mock the import and prevent any dependent code from executing.

import { UserService } from './UserService';

For example if there is a console.log() in the UserService constructor it should not be run because the import is a completely mocked import.

Update:

I've attempted to use jest.mock but they didn't seem to actually mock my imports. I've added a console.log in the UserService constructor and it continues to be triggered when using jest.mock.

import signup from './signup';

jest.mock('./UserService');

describe('signup', () => {
  it('should complete the happy path', async () => {
    const req: IncomingMessage = {} as unknown as IncomingMessage;
    const res: ServerResponse = {
      end: jest.fn(),
    } as unknown as ServerResponse;

    const actual = await signup(req, res);
    expect(actual).toBe(...);
  });
});


Solution 1:[1]

If you mean mocking in a jest test then you can just use jest.mock('./userService'); or jest.spyOn(UserService, 'findUser') if you want to mock one method.

If you want a mock for a specific use case, you would create a mock service and conditionally import based on some flag.

Ex:

// UserService.js
export class UserService {
  constructor(...) {}

  async findUser({ email }: Pick<IUserModel, 'email'>) {
    // calls real API
  }
}


// UserService.mock.js
export class MockUserService {
  constructor(...) {}

  async findUser({ email }: Pick<IUserModel, 'email'>) {
    // calls fake API or just returns a promise or anything you want
  }
}

// Your Endpoint
import { NextApiRequest, NextApiResponse } from 'next';
import nc from 'next-connect';
import { UserService } from './UserService';
import { MockUserService } from './UserService.mock'; // Mock 

let userService;

if (someCondition) {
  userService = new UserService();
} else {
  userService = new MockUserService();
}
...

// The idea is you want to dynamically change what you're importing 
 
const Something = process.env.NODE_ENV === 'development' ?
  require('./FakeSomething') : require('./RealSomething');

Solution 2:[2]

jest.mock doesn't support mocking dependencies for imports outside of the actual test file (.spec.ts & .test.ts). In my case I have a lot of dependencies in my Next API endpoint that cannot be mocked.

You'll have to find a pattern to inject the dependency into the endpoint and test it in isolation instead. I made use of the Nextjs middleware example here but there are likely other patterns that will work just as well.

// Helper method to wait for a middleware to execute before continuing
// And to throw an error when an error happens in a middleware
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: Function) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result)
      }

      return resolve(result)
    })
  })
}

// Testable function for DI
export function post(
  _userService: UserService,
) {
  return async function(req: NextApiRequest, res: NextApiResponse) {
    ...
  }
}

// Endpoint
export default async function(req: NextApiRequest, res: NextApiResponse) {
  try {
    await runMiddleware(req, res, post(userService));

    return res.status(200).send({ done: true });
  } catch (error: any) {
    return res.status(500).end(error.message);
  }
};

// Test usage
describe('signup', () => {
  let userService: UserService;

  beforeAll(() => {
    userService = new UserService({} as unknown as UserRepository);
  });

  it('should complete the happy path', async () => {
    const findSpy = jest
      .spyOn(userService, 'find')
      .mockImplementation(async () => null);

    const req: NextApiRequest = {
      body: {
        email: '[email protected]',
        password: '12345678',
      },
    } as unknown as NextApiRequest;

    const res: NextApiResponse = {} as unknown as NextApiResponse;

    await post(userService)(req, res);

    ...
  });
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 SoluableNonagon
Solution 2