Testing LWC with Jest: The Patterns That Actually Work in CI/CD
Jest testing for LWC is powerful but the setup and patterns are different from standard JavaScript testing. The key insight is that LWC Jest tests run in a simulated DOM (jsdom), not a real browser. Wire adapters are mocked. Apex calls are mocked. Navigation is mocked. You are testing your component logic and DOM manipulation, not Salesforce connectivity.
Start with @salesforce/sfdx-lwc-jest which gives you the test infrastructure. Mock your wire adapters using the emit pattern: import the adapter, call emit() with test data, then assert the DOM rendered correctly. For imperative Apex, mock the module and resolve/reject the promise. The critical pattern most developers miss is flushing the microtask queue — after emitting data or triggering events, you must await a Promise.resolve() (or use the flushPromises helper) before asserting DOM changes.
Structure your tests around user interactions. Instead of testing internal methods, simulate what a user does: click a button, enter text, select an option. Then assert what the user would see: rendered text, CSS classes, visible elements. This makes tests resilient to refactoring. I run LWC Jest tests in GitHub Actions on every PR and they catch regressions that would otherwise reach UAT.
Code Example
// __tests__/contactList.test.js
import { createElement } from 'lwc';
import ContactList from 'c/contactList';
import getContacts from '@salesforce/apex/ContactController.getContacts';
// Mock the Apex wire adapter
jest.mock(
'@salesforce/apex/ContactController.getContacts',
() => ({ default: jest.fn() }),
{ virtual: true }
);
// Helper: flush microtask queue
function flushPromises() {
return new Promise(resolve => setTimeout(resolve, 0));
}
describe('c-contact-list', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
jest.clearAllMocks();
});
it('renders contacts when wire returns data', async () => {
const element = createElement('c-contact-list', { is: ContactList });
document.body.appendChild(element);
// Emit mock data through the wire adapter
getContacts.emit([
{ Id: '001', Name: 'John Doe', Email: 'john@test.com' },
{ Id: '002', Name: 'Jane Smith', Email: 'jane@test.com' }
]);
// CRITICAL: flush the microtask queue
await flushPromises();
const rows = element.shadowRoot.querySelectorAll('tr');
expect(rows.length).toBe(2);
expect(rows[0].textContent).toContain('John Doe');
});
it('shows error when wire fails', async () => {
const element = createElement('c-contact-list', { is: ContactList });
document.body.appendChild(element);
getContacts.error({ message: 'Server error' });
await flushPromises();
const errorEl = element.shadowRoot.querySelector('.error');
expect(errorEl).not.toBeNull();
});
});Need this implemented in your org?
I've shipped these patterns in production for 10+ years.
View Consulting →Enjoyed this? Get more like it.
Glen's Musings — AI, investing, and building things. Occasional. Free.
More LWC Tips
LWC: Wire Service vs Imperative Apex — When to Use Each
Lightning Web Components give you two ways to call Apex: the wire service and imperative calls. The wire service is reac...
Read moreAdvancedLWC Event Handling: Parent-Child, Sibling, and Cross-DOM Communication
Event handling in LWC is where most developers get stuck. The framework gives you three patterns and choosing the wrong ...
Read more