How to Mock Howler.js in Jest

Testing audio functionality in web applications can be challenging because JSDOM, the default environment for Jest, does not support HTML5 Audio or the Web Audio API. This article provides a straightforward, step-by-step guide on how to mock the popular audio library howler.js in Jest, allowing you to run your unit tests seamlessly without encountering environment-related audio errors.

Why You Need to Mock Howler.js

By default, howler.js attempts to access audio APIs like window.AudioContext or HTML5 Audio elements. Since these APIs do not exist in Node.js or JSDOM, calling new Howl() in your test environment will cause your tests to crash or throw errors. Mocking allows you to simulate the behavior of the Howl class and assert that your application calls its methods (like .play() or .stop()) correctly.

Step 1: Create the Howler Mock

You can mock howler.js globally by creating a manual mock in your project’s __mocks__ directory, or inline inside your test file. The global mock approach is recommended if you use Howler across multiple test suites.

Create a file named howler.js inside a folder named __mocks__ in your project’s root directory (or adjacent to your node_modules folder if config allows):

// __mocks__/howler.js

const mockPlay = jest.fn().mockReturnValue(1001); // Returns a fake sound ID
const mockPause = jest.fn();
const mockStop = jest.fn();
const mockUnload = jest.fn();
const mockOn = jest.fn((event, callback) => {
  // Automatically trigger 'load' event to simulate successful audio loading
  if (event === 'load') {
    callback();
  }
});
const mockOnce = jest.fn();
const mockOff = jest.fn();
const mockVolume = jest.fn();

class MockHowl {
  constructor(options) {
    this.options = options;
  }
  play = mockPlay;
  pause = mockPause;
  stop = mockStop;
  unload = mockUnload;
  on = mockOn;
  once = mockOnce;
  off = mockOff;
  volume = mockVolume;
}

module.exports = {
  Howl: MockHowl,
  Howler: {
    volume: jest.fn(),
    mute: jest.fn(),
    unload: jest.fn(),
  },
};

Step 2: Use the Mock in Your Tests

With the mock created, Jest will automatically use it whenever a file imports howler. In your test file, you must explicitly tell Jest to mock the module, then you can write your assertions.

Here is an example of testing a React component or utility function that triggers audio playback:

import { Howl } from 'howler';
import { playSoundEffect } from './audioManager'; // Your custom audio trigger function

// Tell Jest to use the mocked version of howler
jest.mock('howler');

describe('Audio playback tests', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  test('should trigger Howl.play when playSoundEffect is called', () => {
    playSoundEffect('click.mp3');

    // Access the mocked Howl class instance mock methods
    const mockHowlInstance = Howl.mock.instances[0];
    
    expect(Howl).toHaveBeenCalledWith(expect.objectContaining({
      src: ['click.mp3']
    }));
    expect(mockHowlInstance.play).toHaveBeenCalledTimes(1);
  });
});

Inline Mock Alternative

If you only need to mock Howler in a single test file and do not want to create a __mocks__ folder, you can define the mock directly at the top of your test file:

jest.mock('howler', () => {
  return {
    Howl: jest.fn().mockImplementation(() => ({
      play: jest.fn(),
      pause: jest.fn(),
      stop: jest.fn(),
      unload: jest.fn(),
      on: jest.fn(),
      once: jest.fn(),
      off: jest.fn(),
    })),
    Howler: {
      volume: jest.fn(),
    },
  };
});