Khaled
Khaled

Reputation: 8583

Mocking a required class without actually importing it in Jest test

Using Jest example, the required class

// sound-player.js
export default class SoundPlayer {
  constructor() {    
    // do something
  }
}

and the class being tested:

// sound-player-consumer.js
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
  playSomethingCool() {
    this.soundPlayer = new SoundPlayer();
  }
}

I want to test if SoundPlayerConsumer ever called (created an object of) SoundPlayer on playSomethingCool(). My understanding, it will look something like this:

import SoundPlayer from './sound-player';
import SoundPlayerConsumer from './sound-player-consumer';
jest.mock('./sound-player');

beforeEach(() => {
  SoundPlayer.mockClear();
});

it('check if the consumer is called', () => {
  const soundPlayerConsumer = new SoundPlayerConsumer();
  soundPlayerConsumer.playSomethingCool();
  expect(SoundPlayer).toHaveBeenCalledTimes(1);
});

However, in my case I don't want to import ./sound-player as it has many requirements and dependencies which is an overkill for my test, thus I just want to manually mock the class. Here's what I tried so far:

  import SoundPlayerConsumer from './sound-player-consumer'; 
  const SoundPlayer = jest.mock('./sound-player', () => {
    return function() {
      return {}
    }
  });
  it('check if the consumer is called', () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    soundPlayerConsumer.playSomethingCool();
    expect(SoundPlayer).toHaveBeenCalledTimes(1);
  });

but this is the result I get Matcher error: received value must be a mock or spy function I have tried a couple other variations but I always ended up with the same result.

I have read into Jest Manual Mocks, but couldn't fully grasp it.

Upvotes: 0

Views: 2391

Answers (2)

Mitch Lillie
Mitch Lillie

Reputation: 2407

Per the Jest docs, what you want to do is not possible:

Modules that are mocked with jest.mock are mocked only for the file that calls jest.mock. Another file that imports the module will get the original implementation even if it runs after the test file that mocks the module.

So you're left with various kinds of dependency injection or other refactors. One option might be:

// sound-player-consumer.js
import SoundPlayer from './sound-player';

export default class SoundPlayerConsumer {
  constructor({soundPlayer: Soundplayer}) {
    this.soundPlayer = new soundPlayer();
  }
  playSomethingCool(/* or you could inject SoundPlayer here */) {
    // etc
  }
}

And then in your tests, you can build a fake SoundPlayer and pass it in and test it as you like.

  import SoundPlayerConsumer from './sound-player-consumer'; 
  class FakeSoundPlayer {}
  it('check if the consumer is called', () => {
    const soundPlayerConsumer = new SoundPlayerConsumer({soundPlayer: FakeSoundPlayer});
    soundPlayerConsumer.playSomethingCool();
    expect(SoundPlayer).toHaveBeenCalledTimes(1);
  });

Upvotes: 1

Khaled
Khaled

Reputation: 8583

According to Jest docs, mockImplementation can be used to mock class constructors. Thus, you may mock a class and return jest function using mockImplementation.

  import SoundPlayerConsumer from './sound-player-consumer';

  // must have a 'mock' prefix
  const mockSoundPlayer = jest.fn().mockImplementation(() => {
    return {}
  });

  const SoundPlayer = jest.mock('./sound-player', () => {
    return mockSoundPlayer
  });

  it('check if the consumer is called', () => {
    const soundPlayerConsumer = new SoundPlayerConsumer();
    soundPlayerConsumer.playSomethingCool();
    expect(SoundPlayer).toHaveBeenCalledTimes(1);
  });

Upvotes: 1

Related Questions