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-globalRequire 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:
- Clear Event Listeners: Call
$(document).off()or unbind specific containers inafterEachif listeners are attached to the globaldocumentorwindow. - Empty the DOM: Always clear
document.body.innerHTML = ''between runs. - Restore Mocks: Ensure all spies, stubs, and mocked
functions are restored using
jest.restoreAllMocks()orsinon.restore().