LWC components are tested with Jest, wired up via the official @salesforce/sfdx-lwc-jest package. The tests run in a Node environment with JSDOM — no browser, no org, no deploy required. A full suite of 200 component tests finishes in seconds.
Project setup
# Inside your SFDX project root
npm install --save-dev @salesforce/sfdx-lwc-jest
// package.json
{
"scripts": {
"test:unit": "sfdx-lwc-jest",
"test:unit:watch": "sfdx-lwc-jest --watch",
"test:unit:coverage": "sfdx-lwc-jest --coverage"
}
}
Tests live alongside the component in a __tests__ folder:
force-app/main/default/lwc/contactCard/
contactCard.html
contactCard.js
contactCard.js-meta.xml
__tests__/
contactCard.test.js
The anatomy of an LWC test
import { createElement } from 'lwc';
import ContactCard from 'c/contactCard';
describe('c-contact-card', () => {
afterEach(() => {
// Tear down after every test so DOM state doesn't leak.
document.body.innerHTML = '';
});
it('renders the contact name', async () => {
// 1. Create the component
const element = createElement('c-contact-card', { is: ContactCard });
element.contact = { Id: '003xx', Name: 'Ada Lovelace' };
// 2. Mount it
document.body.appendChild(element);
// 3. Wait for the render
await Promise.resolve();
// 4. Assert on the DOM
const heading = element.shadowRoot.querySelector('h2');
expect(heading.textContent).toBe('Ada Lovelace');
});
});
Four patterns to absorb here:
createElement('c-name', { is: Component })— the test factory. Mirrors how the framework instantiates components at runtime.- Set props before append. Assigning to
element.contactbeforeappendChildis faster than after because it avoids an extra render pass. await Promise.resolve()advances one microtask — long enough for LWC’s scheduled render to flush. For multiple re-renders, await multiple times or useflushPromises().element.shadowRoot.querySelector(...)— every component lives in its shadow root in tests, so always go throughshadowRoot.
Asserting on events
it('fires a contactselect event when clicked', async () => {
const element = createElement('c-row-item', { is: RowItem });
element.contact = { Id: '003', Name: 'Grace Hopper' };
document.body.appendChild(element);
const handler = jest.fn();
element.addEventListener('contactselect', handler);
element.shadowRoot.querySelector('li').click();
await Promise.resolve();
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0][0].detail.contactId).toBe('003');
});
Mocking @wire data
For @wire, sfdx-lwc-jest ships wire adapters you can register on:
import { createElement } from 'lwc';
import { getRecord } from 'lightning/uiRecordApi';
import ContactView from 'c/contactView';
// In sfdx-lwc-jest, getRecord is auto-mocked as a TestWireAdapter.
const mockRecord = {
fields: { Name: { value: 'Ada Lovelace' }, Email: { value: 'ada@example.com' } }
};
it('renders the wired contact', async () => {
const element = createElement('c-contact-view', { is: ContactView });
element.recordId = '003xx';
document.body.appendChild(element);
// Emit data into the wire
getRecord.emit(mockRecord);
await Promise.resolve();
const name = element.shadowRoot.querySelector('.name').textContent;
expect(name).toBe('Ada Lovelace');
});
getRecord.emit(...) pushes a value into the wire as if the server responded. There’s also .error(err) to test error paths.
Mocking imperative Apex
Stub the Apex import with Jest’s mock factory:
jest.mock(
'@salesforce/apex/ContactController.upsertContact',
() => ({ default: jest.fn() }),
{ virtual: true }
);
import upsertContact from '@salesforce/apex/ContactController.upsertContact';
it('calls Apex on save', async () => {
upsertContact.mockResolvedValue({ Id: '003xx', Name: 'Saved' });
// ... mount, fill, click save, assert
});
The { virtual: true } flag tells Jest the module doesn’t exist on disk — @salesforce/... imports are resolved at compile time by the LWC tooling.
What sfdx-lwc-jest gives you out of the box
- Automatic mocks for every base Lightning component —
<lightning-input>renders as a stub so you don’t pull in the entire base library. - Automatic mocks for every
lightning/module —uiRecordApi,messageService,navigation,platformShowToastEvent. - A JSDOM environment with shadow DOM polyfilled.
- Coverage reporting built in.
Common interview follow-ups
- “Why do tests need
await Promise.resolve()after a state change?” — LWC batches renders on the microtask queue. Without the await, you assert against pre-render DOM. - “How do you test
disconnectedCallback?” — calldocument.body.removeChild(element)and assert that listeners or subscriptions were cleaned up. - “What’s the coverage requirement for LWC?” — Salesforce doesn’t enforce a hard percentage like the 75% Apex rule, but most teams target 80%+. Lightning Web Components must compile and pass lint; their tests aren’t blocked by org deploys.
- “Can Jest test Apex?” — no. Jest tests are for the client. Apex needs its own unit tests in Apex.
Verified against: LWC Developer Guide — Test Lightning Web Components, @salesforce/sfdx-lwc-jest README. Last reviewed 2026-05-17 for Spring ‘26 release.