Salesforce · · 23 min read

LWC Jest Testing

A complete guide to unit testing Lightning Web Components with Jest — setup, writing basic tests, beforeEach and afterEach, testing events, conditional rendering, iterators, wire adapters, Apex calls, child components, and CI/CD integration.

Part 94: LWC Jest Testing

If you have been building Lightning Web Components through this series, you have written a lot of code that runs inside a browser on top of the Salesforce platform. You have tested that code by deploying it, dropping it onto a page, clicking around, and checking whether things look correct. That approach works until it does not. A change to one component breaks another. An Apex method returns a different shape of data after a refactor. A conditional block stops rendering because someone renamed a property. You do not find out until a user reports a bug or, worse, until the problem has been live for days.

Unit testing solves this. Instead of deploying your code and testing it manually in a browser, you write small, focused tests that verify individual pieces of your component logic. Those tests run locally on your machine in milliseconds, without needing a Salesforce org, a browser, or a network connection. They tell you immediately whether your code does what you expect, and they keep telling you every time you run them after making changes.

We covered Apex unit testing back in Part 66, where you learned how to write test classes for your server-side logic. We also discussed CI/CD pipelines in Part 70, where you saw how automated testing fits into the deployment process. This post brings that same testing discipline to the front end. LWC uses Jest as its testing framework, and Salesforce provides a set of utilities on top of Jest that make it possible to test components in isolation, mock wire adapters, simulate user interactions, and verify DOM output.

This post covers:

  1. What is Jest Testing and why is it important — The case for front-end unit testing.
  2. How to setup your machine and SF project to run jest tests — Installing Node, sfdx-lwc-jest, and configuring your project.
  3. How to create and run a basic jest test for an LWC — Writing your first test file and running it.
  4. The beforeEach method — Setting up fresh component state before each test.
  5. Testing events and conditional rendering — Verifying that events fire and DOM elements appear or hide based on conditions.
  6. The afterEach method — Cleaning up the DOM after tests run.
  7. Testing iterators — Verifying components that use for:each to render lists.
  8. Testing Apex wire methods — Mocking @wire calls that pull data from Apex.
  9. Testing standard wire methods — Mocking platform wire adapters like getRecord.
  10. Testing Apex method calls — Mocking imperative Apex calls.
  11. Testing a child LWC — Stubbing child components to isolate the parent under test.
  12. How to incorporate jest tests into your CI/CD pipeline — Running tests automatically on every push.
  13. Section Notes — Key takeaways.

Let’s get into it.


What Is Jest Testing and Why Is It Important?

Jest is a JavaScript testing framework created by Meta. It runs in Node.js, not in a browser, and it provides everything you need to write and execute tests: a test runner, an assertion library, mocking utilities, and code coverage reporting. When Salesforce chose a testing framework for LWC, they picked Jest because of its speed, its developer experience, and its ability to work with a virtual DOM rather than requiring a real browser.

A Jest test for an LWC component does the following: it creates an instance of your component in a simulated DOM environment, feeds it data (either directly through properties or through mocked wire adapters), and then asserts that the component produced the correct output. That output could be rendered HTML elements, dispatched events, class names on an element, or the absence of an element that should be hidden.

Why does this matter? Three reasons.

First, speed. A Jest test suite with fifty tests runs in a few seconds. Deploying to an org and clicking through the same fifty scenarios takes thirty minutes on a good day. That speed difference changes how you develop. When tests are fast, you run them constantly, and you catch mistakes the moment you make them.

Second, isolation. Jest tests do not depend on an org, a network, or other components. If a test fails, you know the problem is in the code you are testing, not in some upstream dependency. This makes bugs easier to find and fix.

Third, confidence in refactoring. When you have tests covering a component’s behavior, you can restructure the internal code without worrying about accidentally breaking something. The tests will catch regressions before they reach production.

If you wrote Apex test classes in Part 66, the concept is the same. You are verifying that a unit of code produces the right output for a given input. The difference is that here you are testing JavaScript and DOM rendering instead of Apex and DML operations.


How to Setup Your Machine and SF Project to Run Jest Tests

Before you can write tests, you need Node.js installed on your machine and the sfdx-lwc-jest package installed in your Salesforce project.

First, verify that Node.js is installed. Open a terminal and run:

node --version

You need Node.js version 14.x or later. If you do not have it, download it from nodejs.org and install it.

Next, navigate to the root of your Salesforce DX project (the folder that contains sfdx-project.json) and install the LWC Jest package:

npm install --save-dev @salesforce/sfdx-lwc-jest

This installs Jest along with Salesforce-specific test utilities. After the install finishes, open your package.json file and add a test script if one does not already exist:

{
  "scripts": {
    "test:unit": "sfdx-lwc-jest",
    "test:unit:watch": "sfdx-lwc-jest --watch",
    "test:unit:coverage": "sfdx-lwc-jest --coverage"
  }
}

The test:unit script runs all tests once. The test:unit:watch script runs tests in watch mode, re-running them automatically whenever you save a file. The test:unit:coverage script generates a code coverage report.

Your project structure for tests follows a convention. For a component located at force-app/main/default/lwc/myComponent/, you create a __tests__ folder inside that component’s directory:

force-app/
  main/
    default/
      lwc/
        myComponent/
          myComponent.html
          myComponent.js
          myComponent.js-meta.xml
          __tests__/
            myComponent.test.js

The test file must end with .test.js. Jest is configured to find files in __tests__ directories automatically.

Make sure your .forceignore file includes the __tests__ folders so they do not get deployed to your org:

**/__tests__/**

Run a quick check to confirm everything is working:

npm run test:unit

If you have no test files yet, Jest will report that no tests were found. That is expected and means your setup is correct.


How to Create and Run a Basic Jest Test for an LWC

Let’s start with a simple component and write a test for it. Suppose you have a component called greeting that displays a greeting message based on a public property.

Here is the component JavaScript:

// greeting.js
import { LightningElement, api } from 'lwc';

export default class Greeting extends LightningElement {
    @api name = 'World';

    get message() {
        return `Hello, ${this.name}!`;
    }
}

And the template:

<!-- greeting.html -->
<template>
    <p class="greeting-text">{message}</p>
</template>

Now create the test file at __tests__/greeting.test.js:

import { createElement } from 'lwc';
import Greeting from 'c/greeting';

describe('c-greeting', () => {
    it('displays default greeting when no name is provided', () => {
        const element = createElement('c-greeting', {
            is: Greeting
        });
        document.body.appendChild(element);

        const paragraph = element.shadowRoot.querySelector('p.greeting-text');
        expect(paragraph.textContent).toBe('Hello, World!');
    });

    it('displays custom greeting when name is set', () => {
        const element = createElement('c-greeting', {
            is: Greeting
        });
        element.name = 'Salesforce';
        document.body.appendChild(element);

        const paragraph = element.shadowRoot.querySelector('p.greeting-text');
        expect(paragraph.textContent).toBe('Hello, Salesforce!');
    });
});

There are a few things to notice. You import createElement from the lwc module, which creates a component instance in the test DOM. You import the component class directly from its module path. The describe block groups related tests. Each it block is a single test case. Inside each test, you create the element, optionally set properties, append it to the document body, and then query the shadow DOM to verify the output.

Run the test:

npm run test:unit

Jest will find the test file, execute it, and report whether the assertions passed or failed.

One important detail: when you set a property on a component and then immediately query the DOM, the DOM might not have re-rendered yet. LWC batches DOM updates asynchronously. For cases where you need to wait for a re-render, use a resolved promise:

it('updates greeting after property change', async () => {
    const element = createElement('c-greeting', {
        is: Greeting
    });
    element.name = 'Admin';
    document.body.appendChild(element);

    // Wait for async DOM update
    await Promise.resolve();

    const paragraph = element.shadowRoot.querySelector('p.greeting-text');
    expect(paragraph.textContent).toBe('Hello, Admin!');
});

The await Promise.resolve() pattern flushes the microtask queue and allows the component to re-render before you make assertions.


The beforeEach Method

When you have multiple tests for the same component, you end up repeating the same setup code in every test: creating the element, appending it to the body, and sometimes setting initial properties. The beforeEach function runs before every it block inside a describe, which lets you centralize that setup.

import { createElement } from 'lwc';
import Greeting from 'c/greeting';

describe('c-greeting', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-greeting', {
            is: Greeting
        });
        document.body.appendChild(element);
    });

    it('displays default greeting', () => {
        const paragraph = element.shadowRoot.querySelector('p.greeting-text');
        expect(paragraph.textContent).toBe('Hello, World!');
    });

    it('displays custom greeting after property change', async () => {
        element.name = 'Developer';
        await Promise.resolve();

        const paragraph = element.shadowRoot.querySelector('p.greeting-text');
        expect(paragraph.textContent).toBe('Hello, Developer!');
    });

    it('renders a paragraph element', () => {
        const paragraph = element.shadowRoot.querySelector('p');
        expect(paragraph).not.toBeNull();
    });
});

The beforeEach block creates a fresh component instance before every test. This is critical because tests must be independent of each other. If one test modifies the component’s state, those modifications must not leak into the next test. By creating a new element in beforeEach, you guarantee that every test starts with a clean slate.

You can also use beforeEach to set up mock data, configure global state, or perform any other initialization your tests need. Just keep the setup focused on what the entire describe block needs. If only one test needs a specific configuration, do that setup inside the individual it block.


Testing Events and Conditional Rendering

LWC components often dispatch custom events and show or hide elements based on conditions. Both of these behaviors are straightforward to test.

Testing Custom Events

Suppose you have a button component that dispatches a select event when clicked:

// customButton.js
import { LightningElement, api } from 'lwc';

export default class CustomButton extends LightningElement {
    @api label;

    handleClick() {
        this.dispatchEvent(new CustomEvent('select', {
            detail: { label: this.label }
        }));
    }
}

To test that the event fires with the correct data:

import { createElement } from 'lwc';
import CustomButton from 'c/customButton';

describe('c-custom-button', () => {
    it('dispatches select event on click', () => {
        const element = createElement('c-custom-button', {
            is: CustomButton
        });
        element.label = 'Click Me';
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener('select', handler);

        const button = element.shadowRoot.querySelector('button');
        button.click();

        expect(handler).toHaveBeenCalledTimes(1);
        expect(handler.mock.calls[0][0].detail).toEqual({
            label: 'Click Me'
        });
    });
});

The jest.fn() creates a mock function that records every time it is called and what arguments it received. You attach it as an event listener, trigger the click, and then assert that the handler was called with the right payload.

Testing Conditional Rendering

When a component uses if:true or if:false to conditionally render elements, you verify the presence or absence of those elements in the shadow DOM.

Consider a component that shows an error message only when an errorMessage property is set:

// statusDisplay.js
import { LightningElement, api } from 'lwc';

export default class StatusDisplay extends LightningElement {
    @api errorMessage;

    get hasError() {
        return !!this.errorMessage;
    }
}
<!-- statusDisplay.html -->
<template>
    <div class="status-container">
        <template if:true={hasError}>
            <p class="error-text">{errorMessage}</p>
        </template>
        <template if:false={hasError}>
            <p class="success-text">All clear</p>
        </template>
    </div>
</template>

The test:

import { createElement } from 'lwc';
import StatusDisplay from 'c/statusDisplay';

describe('c-status-display', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-status-display', {
            is: StatusDisplay
        });
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('shows success text when no error', () => {
        document.body.appendChild(element);

        const success = element.shadowRoot.querySelector('.success-text');
        const error = element.shadowRoot.querySelector('.error-text');

        expect(success).not.toBeNull();
        expect(success.textContent).toBe('All clear');
        expect(error).toBeNull();
    });

    it('shows error text when errorMessage is set', async () => {
        element.errorMessage = 'Something went wrong';
        document.body.appendChild(element);
        await Promise.resolve();

        const success = element.shadowRoot.querySelector('.success-text');
        const error = element.shadowRoot.querySelector('.error-text');

        expect(error).not.toBeNull();
        expect(error.textContent).toBe('Something went wrong');
        expect(success).toBeNull();
    });
});

The key pattern here is querying for elements that should exist and asserting they are not null, and querying for elements that should not exist and asserting they are null.


The afterEach Method

You may have noticed the afterEach block in the previous example. It runs after every it block and cleans up the DOM by removing all child elements from the document body.

afterEach(() => {
    while (document.body.firstChild) {
        document.body.removeChild(document.body.firstChild);
    }
});

This cleanup is essential. Jest runs all tests in the same global environment. If you append a component to document.body in one test and do not remove it, that component is still there when the next test runs. This causes test pollution, where one test’s leftover state affects another test’s results. You might get false passes because the DOM contains elements from a previous test, or false failures because conflicting elements interfere with queries.

The while loop pattern is the standard approach for LWC Jest tests. It removes every child from the body, regardless of how many elements were appended. This handles edge cases where a test creates multiple components.

You can also use afterEach to reset module-level mocks, clear timers, or undo any global changes your tests made:

afterEach(() => {
    while (document.body.firstChild) {
        document.body.removeChild(document.body.firstChild);
    }
    jest.clearAllMocks();
});

The jest.clearAllMocks() call resets all mock functions, clearing their call history and any configured return values. This prevents mock state from leaking between tests.

Make it a habit to include afterEach in every test file. Skipping it is the most common cause of flaky tests in LWC projects.


Testing Iterators

Components that render lists using for:each need tests that verify the correct number of items are rendered and that each item contains the right data.

Suppose you have a contact list component:

// contactList.js
import { LightningElement, api } from 'lwc';

export default class ContactList extends LightningElement {
    @api contacts = [];
}
<!-- contactList.html -->
<template>
    <ul class="contact-list">
        <template for:each={contacts} for:item="contact">
            <li key={contact.Id} class="contact-item">
                {contact.Name}
            </li>
        </template>
    </ul>
</template>

The test:

import { createElement } from 'lwc';
import ContactList from 'c/contactList';

const MOCK_CONTACTS = [
    { Id: '001', Name: 'Alice Johnson' },
    { Id: '002', Name: 'Bob Smith' },
    { Id: '003', Name: 'Carol Davis' }
];

describe('c-contact-list', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-contact-list', {
            is: ContactList
        });
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('renders no list items when contacts is empty', () => {
        document.body.appendChild(element);

        const items = element.shadowRoot.querySelectorAll('.contact-item');
        expect(items.length).toBe(0);
    });

    it('renders correct number of list items', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        const items = element.shadowRoot.querySelectorAll('.contact-item');
        expect(items.length).toBe(3);
    });

    it('renders contact names in correct order', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        const items = element.shadowRoot.querySelectorAll('.contact-item');
        expect(items[0].textContent).toBe('Alice Johnson');
        expect(items[1].textContent).toBe('Bob Smith');
        expect(items[2].textContent).toBe('Carol Davis');
    });

    it('updates list when contacts property changes', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        element.contacts = [{ Id: '004', Name: 'Dan Wilson' }];
        await Promise.resolve();

        const items = element.shadowRoot.querySelectorAll('.contact-item');
        expect(items.length).toBe(1);
        expect(items[0].textContent).toBe('Dan Wilson');
    });
});

Notice that the mock data is defined as a constant outside the describe block. This keeps the tests clean and makes it easy to reuse the same data across multiple test cases. When testing iterators, always test the empty case, the populated case, and the update case.


Testing Apex Wire Methods

When a component uses @wire to call an Apex method, the test needs to mock that wire adapter. Salesforce provides utilities for this. You create a mock for the Apex method, and then in your test, you emit data or errors through that mock.

Suppose you have a component wired to an Apex method:

// accountList.js
import { LightningElement, wire } from 'lwc';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

export default class AccountList extends LightningElement {
    accounts;
    error;

    @wire(getAccounts)
    wiredAccounts({ data, error }) {
        if (data) {
            this.accounts = data;
            this.error = undefined;
        } else if (error) {
            this.error = error;
            this.accounts = undefined;
        }
    }
}

First, create a mock for the Apex method. Create a file at force-app/test/jest-mocks/apex/AccountController.getAccounts.js:

import { createApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
export default createApexTestWireAdapter(jest.fn());

Then configure Jest to resolve the Apex import to this mock. In your jest.config.js:

module.exports = {
    moduleNameMapper: {
        '^@salesforce/apex/AccountController.getAccounts$':
            '<rootDir>/force-app/test/jest-mocks/apex/AccountController.getAccounts.js'
    }
};

Now write the test:

import { createElement } from 'lwc';
import AccountList from 'c/accountList';
import getAccounts from '@salesforce/apex/AccountController.getAccounts';

const MOCK_ACCOUNTS = [
    { Id: '001abc', Name: 'Acme Corp' },
    { Id: '002def', Name: 'Global Industries' }
];

describe('c-account-list', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-account-list', {
            is: AccountList
        });
        document.body.appendChild(element);
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('renders accounts when wire returns data', async () => {
        getAccounts.emit(MOCK_ACCOUNTS);
        await Promise.resolve();

        const items = element.shadowRoot.querySelectorAll('li');
        expect(items.length).toBe(2);
    });

    it('displays error when wire returns error', async () => {
        getAccounts.error({ body: { message: 'Server error' } });
        await Promise.resolve();

        const errorEl = element.shadowRoot.querySelector('.error-message');
        expect(errorEl).not.toBeNull();
    });
});

The emit method simulates a successful wire response. The error method simulates a failure. Both trigger the wire handler in the component, which updates the component state and causes a re-render.


Testing Standard Wire Methods

Standard wire adapters like getRecord, getFieldValue, and getObjectInfo from lightning/ui*Api modules work the same way as Apex wire adapters in tests. Salesforce provides built-in mocks through sfdx-lwc-jest.

Suppose a component uses getRecord:

// accountDetail.js
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import NAME_FIELD from '@salesforce/schema/Account.Name';
import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry';

export default class AccountDetail extends LightningElement {
    @api recordId;

    @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD] })
    account;

    get name() {
        return getFieldValue(this.account.data, NAME_FIELD);
    }

    get industry() {
        return getFieldValue(this.account.data, INDUSTRY_FIELD);
    }
}

The test uses the same emit pattern. The sfdx-lwc-jest package automatically mocks lightning/uiRecordApi:

import { createElement } from 'lwc';
import AccountDetail from 'c/accountDetail';
import { getRecord } from 'lightning/uiRecordApi';

const MOCK_RECORD = {
    fields: {
        Name: { value: 'Acme Corp' },
        Industry: { value: 'Technology' }
    }
};

describe('c-account-detail', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-account-detail', {
            is: AccountDetail
        });
        element.recordId = '001abc';
        document.body.appendChild(element);
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('renders account name from wire data', async () => {
        getRecord.emit(MOCK_RECORD);
        await Promise.resolve();

        const nameEl = element.shadowRoot.querySelector('.account-name');
        expect(nameEl.textContent).toBe('Acme Corp');
    });

    it('renders account industry from wire data', async () => {
        getRecord.emit(MOCK_RECORD);
        await Promise.resolve();

        const industryEl = element.shadowRoot.querySelector('.account-industry');
        expect(industryEl.textContent).toBe('Technology');
    });

    it('passes correct recordId to wire adapter', () => {
        expect(getRecord.getLastConfig().recordId).toBe('001abc');
    });
});

The getLastConfig method lets you verify that the component passed the right parameters to the wire adapter. This is useful for confirming that reactive properties like $recordId are correctly feeding into the wire call.

For schema imports like @salesforce/schema/Account.Name, Jest resolves them to simple string values by default. The sfdx-lwc-jest resolver handles this automatically.


Testing Apex Method Calls

Not all Apex calls go through @wire. Many components use imperative Apex calls — importing the method and calling it directly, usually in response to a user action. Testing these calls requires a different approach: you mock the imported Apex method and control what it returns.

// accountCreator.js
import { LightningElement } from 'lwc';
import createAccount from '@salesforce/apex/AccountController.createAccount';

export default class AccountCreator extends LightningElement {
    accountName = '';
    successMessage;
    errorMessage;

    handleNameChange(event) {
        this.accountName = event.target.value;
    }

    async handleSave() {
        try {
            const result = await createAccount({ name: this.accountName });
            this.successMessage = `Created account: ${result.Name}`;
            this.errorMessage = undefined;
        } catch (error) {
            this.errorMessage = error.body.message;
            this.successMessage = undefined;
        }
    }
}

In the test, you mock the module and control its resolved or rejected value:

import { createElement } from 'lwc';
import AccountCreator from 'c/accountCreator';
import createAccount from '@salesforce/apex/AccountController.createAccount';

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

describe('c-account-creator', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-account-creator', {
            is: AccountCreator
        });
        document.body.appendChild(element);
        createAccount.mockClear();
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('displays success message after successful save', async () => {
        createAccount.mockResolvedValue({ Id: '001abc', Name: 'Test Account' });

        const input = element.shadowRoot.querySelector('lightning-input');
        input.value = 'Test Account';
        input.dispatchEvent(new CustomEvent('change'));

        const saveButton = element.shadowRoot.querySelector('lightning-button');
        saveButton.click();

        // Wait for the async Apex call to resolve
        await Promise.resolve();
        await Promise.resolve();

        const successEl = element.shadowRoot.querySelector('.success-message');
        expect(successEl.textContent).toBe('Created account: Test Account');
    });

    it('displays error message when save fails', async () => {
        createAccount.mockRejectedValue({
            body: { message: 'Duplicate account name' }
        });

        const saveButton = element.shadowRoot.querySelector('lightning-button');
        saveButton.click();

        await Promise.resolve();
        await Promise.resolve();

        const errorEl = element.shadowRoot.querySelector('.error-message');
        expect(errorEl.textContent).toBe('Duplicate account name');
    });

    it('calls createAccount with correct parameters', async () => {
        createAccount.mockResolvedValue({ Id: '001abc', Name: 'New Corp' });

        element.shadowRoot.querySelector('lightning-input').value = 'New Corp';
        element.shadowRoot.querySelector('lightning-input')
            .dispatchEvent(new CustomEvent('change'));

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

        await Promise.resolve();

        expect(createAccount).toHaveBeenCalledWith({ name: 'New Corp' });
    });
});

Notice the double await Promise.resolve(). Imperative Apex calls are promises, and the component’s re-render after the promise resolves happens in a separate microtask. Two awaits ensure both the promise resolution and the subsequent DOM update have completed.

The jest.mock call at the top tells Jest to replace the Apex module import with a mock function. The { virtual: true } option is necessary because the module does not actually exist in the file system — it is a Salesforce platform import.


Testing a Child LWC

When a parent component contains child components, you want to test the parent in isolation without dragging in the real child implementation. Jest lets you create stubs for child components.

Suppose a parent component renders a c-contact-card child:

<!-- contactPage.html -->
<template>
    <h1>{title}</h1>
    <template for:each={contacts} for:item="contact">
        <c-contact-card
            key={contact.Id}
            name={contact.Name}
            email={contact.Email}
            onselect={handleContactSelect}>
        </c-contact-card>
    </template>
    <template if:true={selectedContact}>
        <p class="selection">Selected: {selectedContact}</p>
    </template>
</template>

Create a stub for the child component. Place it in a __tests__ directory or a shared mocks folder:

// __tests__/stubs/contactCard.js
import { LightningElement, api } from 'lwc';

export default class ContactCardStub extends LightningElement {
    @api name;
    @api email;
}

Configure Jest to use the stub in jest.config.js:

module.exports = {
    moduleNameMapper: {
        '^c/contactCard$': '<rootDir>/force-app/test/jest-stubs/contactCard.js'
    }
};

Now write the parent’s test:

import { createElement } from 'lwc';
import ContactPage from 'c/contactPage';

const MOCK_CONTACTS = [
    { Id: '003a', Name: 'Alice', Email: 'alice@test.com' },
    { Id: '003b', Name: 'Bob', Email: 'bob@test.com' }
];

describe('c-contact-page', () => {
    let element;

    beforeEach(() => {
        element = createElement('c-contact-page', {
            is: ContactPage
        });
    });

    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('renders correct number of child components', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        const cards = element.shadowRoot.querySelectorAll('c-contact-card');
        expect(cards.length).toBe(2);
    });

    it('passes correct props to child components', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        const cards = element.shadowRoot.querySelectorAll('c-contact-card');
        expect(cards[0].name).toBe('Alice');
        expect(cards[0].email).toBe('alice@test.com');
        expect(cards[1].name).toBe('Bob');
        expect(cards[1].email).toBe('bob@test.com');
    });

    it('handles select event from child', async () => {
        element.contacts = MOCK_CONTACTS;
        document.body.appendChild(element);
        await Promise.resolve();

        const cards = element.shadowRoot.querySelectorAll('c-contact-card');
        cards[0].dispatchEvent(new CustomEvent('select', {
            detail: { name: 'Alice' },
            bubbles: true
        }));
        await Promise.resolve();

        const selection = element.shadowRoot.querySelector('.selection');
        expect(selection.textContent).toBe('Selected: Alice');
    });
});

By stubbing the child, your test only verifies the parent’s behavior: that it renders the right number of children, passes the right props, and handles events correctly. If the child component has a bug, it will not cause the parent’s tests to fail. You test the child separately in its own test file.


How to Incorporate Jest Tests Into Your CI/CD Pipeline

Writing tests is valuable, but running them automatically on every commit or pull request is where the real payoff comes. If you set up CI/CD back in Part 70, adding Jest tests to the pipeline is straightforward.

For a GitHub Actions workflow, add a step that installs Node dependencies and runs the test suite:

name: CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run LWC Jest tests
        run: npm run test:unit -- --ci --coverage

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

The --ci flag tells Jest to run in continuous integration mode, which fails the build if a snapshot does not match instead of updating it. The --coverage flag generates a code coverage report that you can review as a build artifact.

If you use a different CI platform like Bitbucket Pipelines or Azure DevOps, the approach is the same: install Node, install dependencies, run npm run test:unit. The Jest exit code is non-zero when any test fails, so the pipeline step will fail and block the deployment.

You can also enforce a minimum coverage threshold in your jest.config.js:

module.exports = {
    coverageThreshold: {
        global: {
            branches: 80,
            functions: 80,
            lines: 80,
            statements: 80
        }
    }
};

With this configuration, the test command fails if any coverage metric drops below 80 percent. This prevents developers from merging code that is not adequately tested.

For teams working on large projects, consider running tests in parallel and using the --changedSince flag during development to only run tests for files that have changed:

npx sfdx-lwc-jest --changedSince=main

This keeps feedback loops short while still running the full suite in CI.


Section Notes

LWC Jest testing brings the same discipline to your front-end code that Apex test classes bring to your back end. The key points from this section:

Setup is minimal. Install @salesforce/sfdx-lwc-jest, add a test script to package.json, and create __tests__ directories inside your component folders. No browser, no org, no complex configuration.

Every test follows the same pattern. Create the component with createElement, set properties, append to the document body, wait for re-renders with await Promise.resolve(), query the shadow DOM, and assert on the results.

beforeEach and afterEach are not optional. Use beforeEach to create a fresh component instance for each test. Use afterEach to remove all elements from the document body and clear mocks. Skipping cleanup is the leading cause of flaky tests.

Mock everything external. Wire adapters get mocked with createApexTestWireAdapter or the built-in sfdx-lwc-jest resolvers. Imperative Apex calls get mocked with jest.mock and controlled with mockResolvedValue or mockRejectedValue. Child components get stubbed so you test parents in isolation.

Run tests in CI. Add Jest to your CI/CD pipeline so tests run on every commit. Enforce coverage thresholds to maintain quality over time. The combination of fast local tests during development and mandatory tests in the pipeline catches bugs early and prevents regressions.

Test behavior, not implementation. Focus your tests on what the component does from the outside: what it renders, what events it dispatches, how it responds to property changes. Avoid testing internal methods or private state. If you refactor the internals but the external behavior stays the same, your tests should still pass.

If you skipped Apex testing in Part 66 or CI/CD in Part 70, go back and read those sections. Front-end testing does not exist in isolation. A well-tested Salesforce application has Apex test classes covering the server side, Jest tests covering the client side, and a CI/CD pipeline running both automatically. That combination gives you the confidence to ship changes without fear.