Skip to main content

SF-9013 · Scenario · Medium

How do you write unit tests for an LWC using Jest?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

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:

  1. createElement('c-name', { is: Component }) — the test factory. Mirrors how the framework instantiates components at runtime.
  2. Set props before append. Assigning to element.contact before appendChild is faster than after because it avoids an extra render pass.
  3. await Promise.resolve() advances one microtask — long enough for LWC’s scheduled render to flush. For multiple re-renders, await multiple times or use flushPromises().
  4. element.shadowRoot.querySelector(...) — every component lives in its shadow root in tests, so always go through shadowRoot.

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: '[email protected]' } }
};

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/ moduleuiRecordApi, 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?” — call document.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.