Unit Testing jQuery Code with Jest and Mocha

Testing jQuery applications requires simulating a browser Document Object Model (DOM), loading HTML fixtures, executing scripts against those fixtures, and asserting changes in element states, event listeners, or asynchronous requests. This guide details the complete process of setting up a headless DOM environment, writing test cases for DOM manipulation and event handlers, mocking AJAX requests, and managing test cleanup using either Jest or Mocha.

1. Setting Up the Test Environment

Because jQuery requires access to objects like window and document, you must run tests in an environment that provides a simulated DOM.

Using Jest

Jest includes jsdom out of the box. Ensure your jest.config.js specifies the jsdom test environment:

module.exports = {
  testEnvironment: 'jsdom',
};

Using Mocha

Mocha runs on Node.js directly, so you must install and configure jsdom and an assertion library like chai manually:

npm install --save-dev mocha chai jsdom jsdom-global

Require jsdom-global before your test suites run, typically via a Mocha setup file or CLI flag (--require jsdom-global/register).


2. Setting Up DOM Fixtures and Loading jQuery

Unit tests should run against an isolated HTML structure. In both frameworks, you configure the initial markup before executing jQuery selectors.

const $ = require('jquery');

describe('jQuery Component Suite', () => {
  beforeEach(() => {
    // Inject the component's HTML into the document body
    document.body.innerHTML = `
      <div id="wrapper">
        <button id="toggle-btn">Click Me</button>
        <p id="status-text" class="hidden">Inactive</p>
      </div>
    `;
  });

  afterEach(() => {
    // Reset the DOM to avoid test pollution
    document.body.innerHTML = '';
  });
});

3. Testing DOM Manipulation

To test functions that modify elements (e.g., adding classes, changing text, appending children), trigger the target function and assert against the resulting DOM properties.

Target Implementation

function activateStatus() {
  $('#status-text').removeClass('hidden').addClass('active').text('Active');
}

Jest Test

test('activateStatus updates classes and text content', () => {
  activateStatus();

  const $status = $('#status-text');
  expect($status.hasClass('hidden')).toBe(false);
  expect($status.hasClass('active')).toBe(true);
  expect($status.text()).toBe('Active');
});

Mocha and Chai Test

const { expect } = require('chai');

it('activateStatus updates classes and text content', () => {
  activateStatus();

  const $status = $('#status-text');
  expect($status.hasClass('hidden')).to.be.false;
  expect($status.hasClass('active')).to.be.true;
  expect($status.text()).to.equal('Active');
});

4. Testing Event Listeners

Testing jQuery event listeners involves binding the event, dispatching it using jQuery's .trigger() method, and confirming that the intended callback or side effect occurred.

Target Implementation

function bindEvents() {
  $('#toggle-btn').on('click', function () {
    $('#status-text').toggleClass('active');
  });
}

Jest Test

test('clicking #toggle-btn toggles active class on #status-text', () => {
  bindEvents();

  const $btn = $('#toggle-btn');
  const $status = $('#status-text');

  // Trigger click event
  $btn.trigger('click');
  expect($status.hasClass('active')).toBe(true);

  // Trigger second click to verify toggle off
  $btn.trigger('click');
  expect($status.hasClass('active')).toBe(false);
});

To test whether an external callback was invoked, use test spies (jest.fn() in Jest or sinon.spy() in Mocha).


5. Mocking jQuery AJAX Requests

When code issues asynchronous requests using $.ajax, $.get, or $.post, mock the network call to keep tests fast, reliable, and isolated from external APIs.

Target Implementation

function fetchUserData(userId, callback) {
  $.ajax({
    url: `/api/users/${userId}`,
    method: 'GET',
    success: function (data) {
      callback(data);
    },
  });
}

Mocking in Jest

Use jest.spyOn to intercept $.ajax:

test('fetchUserData requests user data and calls callback', () => {
  const mockData = { id: 1, name: 'Alice' };
  const ajaxSpy = jest.spyOn($, 'ajax').mockImplementation((options) => {
    options.success(mockData);
  });

  const callback = jest.fn();
  fetchUserData(1, callback);

  expect(ajaxSpy).toHaveBeenCalledWith(
    expect.objectContaining({
      url: '/api/users/1',
      method: 'GET',
    })
  );
  expect(callback).toHaveBeenCalledWith(mockData);

  ajaxSpy.mockRestore();
});

Mocking in Mocha with Sinon

Use sinon.stub to achieve the same result:

const sinon = require('sinon');

it('fetchUserData requests user data and calls callback', () => {
  const mockData = { id: 1, name: 'Alice' };
  const ajaxStub = sinon.stub($, 'ajax').callsFake((options) => {
    options.success(mockData);
  });

  const callback = sinon.spy();
  fetchUserData(1, callback);

  expect(ajaxStub.calledOnce).to.be.true;
  expect(ajaxStub.firstCall.args[0].url).to.equal('/api/users/1');
  expect(callback.calledWith(mockData)).to.be.true;

  ajaxStub.restore();
});

6. Teardown and Isolation Best Practices

To prevent cross-test contamination:

  1. Clear Event Listeners: Call $(document).off() or unbind specific containers in afterEach if listeners are attached to the global document or window.
  2. Empty the DOM: Always clear document.body.innerHTML = '' between runs.
  3. Restore Mocks: Ensure all spies, stubs, and mocked functions are restored using jest.restoreAllMocks() or sinon.restore().