LearnFrenzy
Sign in Take a free test

Testing and Interview Prep · Lesson 24 of 25

Testing LWC with Jest

6 min read Updated Nov 8, 2026 Saurabh Samir

Apex has to be tested before it can be deployed; LWC doesn't – which is exactly why many teams' components break silently. Jest tests for LWC run on your machine in seconds, without an org: they render a component in a simulated browser, interact with it, and check the result. Salesforce's @salesforce/sfdx-lwc-jest package provides stubs for every lightning-* component and test versions of the wire adapters. The examples in this course were all checked with it.

In this lesson

  • Setup and the shape of a test
  • DOM assertions and simulating clicks
  • Emitting wire data and mocking Apex
  • Testing events and a failing test

Setup

Projects created with sf project generate are already configured. Otherwise:

npm install --save-dev @salesforce/sfdx-lwc-jest
# package.json: "test:unit": "sfdx-lwc-jest", "test:unit:watch": "sfdx-lwc-jest --watch"
npm run test:unit

Tests live next to the component in __tests__/componentName.test.js, which .forceignore keeps out of deployments.

The component under test

<!-- leadScore.html -->
<template>
    <lightning-card title="Lead score">
        <template lwc:if={lead.data}>
            <p class="score">{name}: {score} points</p>
            <p class="grade">{grade}</p>
        </template>
        <template lwc:elseif={lead.error}>
            <p class="error">Could not load the lead</p>
        </template>
        <lightning-button label="Recalculate" onclick={recalculate}></lightning-button>
    </lightning-card>
</template>
// leadScore.js
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import recalculateScore from '@salesforce/apex/LeadScoring.recalculate';
import NAME from '@salesforce/schema/Lead.Name';
import SCORE from '@salesforce/schema/Lead.Score__c';

export default class LeadScore extends LightningElement {
    @api recordId;
    override;

    @wire(getRecord, { recordId: '$recordId', fields: [NAME, SCORE] }) lead;

    get name() {
        return getFieldValue(this.lead.data, NAME);
    }
    get score() {
        return this.override ?? getFieldValue(this.lead.data, SCORE);
    }
    get grade() {
        return this.score >= 80 ? 'Hot' : this.score >= 40 ? 'Warm' : 'Cold';
    }

    async recalculate() {
        this.override = await recalculateScore({ leadId: this.recordId });
        this.dispatchEvent(new CustomEvent('scorechange', { detail: { score: this.override } }));
    }
}

The tests

// __tests__/leadScore.test.js
import { createElement } from 'lwc';
import LeadScore from 'c/leadScore';
import { getRecord } from 'lightning/uiRecordApi';
import recalculateScore from '@salesforce/apex/LeadScoring.recalculate';

// Replace the Apex method with a Jest mock
jest.mock('@salesforce/apex/LeadScoring.recalculate', () => ({ default: jest.fn() }), { virtual: true });

const LEAD = {
    apiName: 'Lead',
    id: '00QXX0000001Lw',
    fields: { Name: { value: 'Ravi Kumar' }, Score__c: { value: 55 } },
};

// let pending promises and re-renders finish
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));

describe('c-lead-score', () => {
    afterEach(() => {
        while (document.body.firstChild) document.body.removeChild(document.body.firstChild);   // clean the DOM
        jest.clearAllMocks();
    });

    function render() {
        const element = createElement('c-lead-score', { is: LeadScore });
        element.recordId = '00QXX0000001Lw';
        document.body.appendChild(element);
        return element;
    }

    it('shows the score and grade from the record', async () => {
        const element = render();
        getRecord.emit(LEAD);                              // deliver data through the wire
        await flushPromises();
        expect(element.shadowRoot.querySelector('.score').textContent).toBe('Ravi Kumar: 55 points');
        expect(element.shadowRoot.querySelector('.grade').textContent).toBe('Warm');
    });

    it('shows an error when the record cannot be loaded', async () => {
        const element = render();
        getRecord.error();
        await flushPromises();
        expect(element.shadowRoot.querySelector('.error')).not.toBeNull();
    });

    it('recalculates through Apex and fires scorechange', async () => {
        recalculateScore.mockResolvedValue(91);
        const element = render();
        const handler = jest.fn();
        element.addEventListener('scorechange', handler);
        getRecord.emit(LEAD);
        await flushPromises();

        element.shadowRoot.querySelector('lightning-button').click();
        await flushPromises();

        expect(recalculateScore).toHaveBeenCalledWith({ leadId: '00QXX0000001Lw' });
        expect(element.shadowRoot.querySelector('.grade').textContent).toBe('Hot');
        expect(handler.mock.calls[0][0].detail).toEqual({ score: 91 });
    });
});

Test run

PASS force-app/main/default/lwc/leadScore/__tests__/leadScore.test.js
✓ shows the score and grade from the record
✓ shows an error when the record cannot be loaded
✓ recalculates through Apex and fires scorechange

Tests: 3 passed, 3 total

The shape of every LWC test

  1. CreatecreateElement('c-name', { is: Component }), set @api props, append to document.body
  2. Feedadapter.emit(data) for wires; mockResolvedValue for imperative Apex
  3. Actclick elements or dispatch events, then await flushPromises()
  4. Assertquery element.shadowRoot, check mocks and received events
  • Wire adapters like getRecord are test adapters in Jest: emit(data) and error() push values into the component.
  • jest.mock(…, { virtual: true }) replaces an Apex import; check calls with toHaveBeenCalledWith.
  • Rendering is asynchronous: always await a resolved promise after changing data before asserting.
  • Clean document.body after each test so tests don't affect each other.

What a failure looks like

// __tests__/leadScore.test.js
// a test with a wrong expectation
import { createElement } from 'lwc';
import LeadScore from 'c/leadScore';
import { getRecord } from 'lightning/uiRecordApi';

jest.mock('@salesforce/apex/LeadScoring.recalculate', () => ({ default: jest.fn() }), { virtual: true });

it('grades a 40-point lead as Cold', async () => {
    const element = createElement('c-lead-score', { is: LeadScore });
    document.body.appendChild(element);
    getRecord.emit({ apiName: 'Lead', fields: { Name: { value: 'Meera' }, Score__c: { value: 40 } } });
    await Promise.resolve();
    expect(element.shadowRoot.querySelector('.grade').textContent).toBe('Cold');
});

Test run

FAIL force-app/main/default/lwc/leadScore/__tests__/leadScore.test.js
✕ grades a 40-point lead as Cold
  Expected: "Cold"
  Received: "Warm"

Tests: 1 failed, 1 total

The test expected "Cold", but the component says a score of exactly 40 is "Warm" (>= 40). Either the test or the requirement is wrong – which is exactly the conversation a failing test should start.

Practice

Try it yourself

  1. Write a test for a counter component: it starts at 0 and shows 1 after clicking "Add".

    Show solution
    <!-- clickCounter.html -->
    <template>
        <p class="count">{count}</p>
        <lightning-button label="Add" onclick={add}></lightning-button>
    </template>
    // clickCounter.js
    import { LightningElement } from 'lwc';
    
    export default class ClickCounter extends LightningElement {
        count = 0;
        add() {
            this.count++;
        }
    }
    // __tests__/clickCounter.test.js
    import { createElement } from 'lwc';
    import ClickCounter from 'c/clickCounter';
    
    describe('c-click-counter', () => {
        it('increments on click', async () => {
            const element = createElement('c-click-counter', { is: ClickCounter });
            document.body.appendChild(element);
            expect(element.shadowRoot.querySelector('.count').textContent).toBe('0');
    
            element.shadowRoot.querySelector('lightning-button').click();
            await Promise.resolve();
            expect(element.shadowRoot.querySelector('.count').textContent).toBe('1');
        });
    });

    Test run

    PASS force-app/main/default/lwc/clickCounter/__tests__/clickCounter.test.js
    ✓ increments on click
    
    Tests: 1 passed, 1 total

Quick check

Do LWC Jest tests need a Salesforce org?

No – they run locally with stubs for base components and wire adapters.

How do you send data to a @wire(getRecord) in a test?

getRecord.emit(data), then await a promise.

Why await after clicking?

LWC re-renders asynchronously; the DOM updates on the next microtask.

Summary

  • sfdx-lwc-jest runs fast, org-free unit tests for components.
  • Create, feed (emit / mockResolvedValue), act, await, assert – and clean up.
  • Test the behaviour users depend on: rendering, errors, Apex calls and events.

Finished this lesson?

Mark it complete to track your progress, then carry on with the next lesson.

Next lesson

Search LearnFrenzy

Esc

Quick links