Develop Secure Code
Install Jest
Run Jest Tests
Write Jest Tests
Write Jest Tests for Wire Service
End-to-End Tests
Jest Test Patterns
Write your component tests in local JavaScript files. Commit them to version control along with the component itself. Jest tests aren’t saved to Salesforce.
Jest tests are written, saved, and run differently than Jasmine or Mocha tests written for the Lightning Testing Service. Jest tests are local only, and are saved and run independently of Salesforce.
You can use the Salesforce CLI command sf force lightning lwc test create to create a test directory and a boilerplate test file within the directory. The following command creates a test directory and file for testing the myButton Lightning web component.
1sf force lightning lwc test create -f force-app/main/default/lwc/myButton/myButton.jsAfter you use the Salesforce CLI create command, you see a folder named __tests__ at the top level of your component’s bundle directory, such as force-app/main/default/lwc/myButton/__tests__. Otherwise, create the folder yourself. Save all tests for this component inside the __tests__ folder. Share tests with other team members or systems by committing the __tests__ folder to version control.
To ensure that the __tests__ folder and its contents are never saved to Salesforce, add this glob pattern to the .forceignore file for each of your projects. The Salesforce CLI command sf force lightning lwc test setup does this task for you.
1**/__tests__/**This pattern ensures that Salesforce DX commands that push, pull, or transform code and metadata ignore the __tests__ folder and its contents.
Jest runs JavaScript files in the __tests__ directory. Test files must have names that end in .js, and we recommend that tests end in .test.js. You can have a single test file with all of your component tests, or you can have multiple files to organize related tests. Test files can be placed in sub folders.
To become an accomplished tester, learn how to use Jest. In particular, learn the syntax of the many matchers provided with Jest. We don’t cover general Jest usage here because the Jest documentation is excellent. We focus on the specifics of using Jest with Lightning web components.
Jest tests for a Lightning web component should test the behavior of a single component in isolation, with minimal dependencies on external components or services.
Let’s look at hello.test.js, which is the test for the hello Lightning web component in the lwc-recipes repo.
1// hello.test.js
2import { createElement } from "lwc";
3import Hello from "c/hello";
4
5describe("c-hello", () => {
6 afterEach(() => {
7 // The jsdom instance is shared across test cases in a single file so reset the DOM
8 while (document.body.firstChild) {
9 document.body.removeChild(document.body.firstChild);
10 }
11 });
12
13 it("displays greeting", () => {
14 // Create element
15 const element = createElement("c-hello", {
16 is: Hello,
17 });
18 document.body.appendChild(element);
19
20 // Verify displayed greeting
21 const div = element.shadowRoot.querySelector("div");
22 expect(div.textContent).toBe("Hello, World!");
23 });
24});Let’s walk through the code and learn about each section of the test file. All tests must use this structure.
First, the test imports the createElement method. This method is available only in tests. The code must also import the component to test, which in this case is c/hello. Use these imports later to create the component under test.
1import { createElement } from "lwc";
2import Hello from "c/hello";A describe block defines a test suite. A test suite contains one or more tests that belong together from a functional point of view.
1describe('c-hello', () => {
2 ...
3});We recommend having a top level describe block with a description matching the component name. Add more describe blocks that group functionality only if necessary.
For hello.test.js, a single describe is sufficient. For more complex components, it may make sense to have several describe blocks that group things into categories like error scenarios, empty input, wired data, regular data, and so on.
The Jest afterEach() method resets the DOM at the end of the test.
Since a browser isn’t running when tests run, Jest uses jsdom to provide an environment that behaves much like a browser’s DOM or document. Jest has a dependency on jsdom, which is a Node.js project, so jsdom is downloaded during installation of the sfdx-lwc-jest project the same way Jest itself is.
Each test file shares a single instance of jsdom, and changes aren’t reset between tests inside the file. Therefore it’s a best practice to clean up between tests, so that a test’s output doesn’t affect any other test.
1afterEach(() => {
2 // The jsdom instance is shared across test cases in a single file so reset the DOM
3 while (document.body.firstChild) {
4 document.body.removeChild(document.body.firstChild);
5 }
6});Jest also has other methods that you can use to perform setup and cleanup tasks. See jestjs.io/docs/en/setup-teardown.
it is an alias for test. Use whichever word allows you to describe the expected behavior accurately.
Note
An it block describes a single test. A test represents a single functional unit that you want to test. Write the it to describe the expected behavior of that function. For example, the hello component displays “Hello, World!”, so the it block tests that the hello component displays a greeting.
1it('displays greeting', () => {
2 ...
3});The test uses the imported createElement method to create an instance of the component to test, in this case, c-hello.
1const element = createElement("c-hello", {
2 is: Hello,
3});The test then calls appendChild to add the component to the test’s version of document.
The appendChild() call inserts the component into the DOM and the lifecycle hooks connectedCallback() and renderedCallback() are called.
1document.body.appendChild(element);The next step is to use a standard DOM query method to search the DOM for the element. Use element.shadowRoot as the parent for the query. It’s a test-only API that lets you peek across the shadow boundary to inspect a component’s shadow tree. It’s the test equivalent of this.template.
1const div = element.shadowRoot.querySelector("div");Finally, the expect statement is an assertion of the success condition: that the text of the element is “Hello, World!”
1const div = element.shadowRoot.querySelector("div");
2expect(div.textContent).toBe("Hello, World!");Jest supports lots of matchers like toBe and toMatchObject that make it easy to check that a value meets a condition. See jestjs.io/docs/en/expect.
To test a component that extends NavigationMixin(LightningElement), create a mock navigation plugin. The sfdx-lwc-jest package includes a lightning/navigation mock by default. To use the mock, you must provide a jest config.
Here’s an example mock that’s used by many navigation components in the lwc-recipes repo.
1/**
2 * For the original lightning/navigation mock that comes by default with
3 * @salesforce/sfdx-lwc-jest, see:
4 * https://github.com/salesforce/sfdx-lwc-jest/blob/master/src/lightning-stubs/navigation/navigation.js
5 */
6
7import { createTestWireAdapter } from '@salesforce/wire-service-jest-util';
8export const CurrentPageReference = createTestWireAdapter(jest.fn());
9
10const Navigate = Symbol('Navigate');
11const GenerateUrl = Symbol('GenerateUrl');
12
13// We need the mock to reset between tests, so we hold the state in jest.fn() objects.
14// We use Jest's reflection methods to implement the two CalledWith helpers below so we don't need to update all the existing tests.
15// A cleaner implementation would just export these two mocks directly for consumption by the test authors to verify the state.
16export const mockNavigate = jest.fn();
17export const mockGenerate = jest.fn();
18
19export const NavigationMixin = (Base) => {
20 return class extends Base {
21 [Navigate](pageReference, replace) {
22 mockNavigate({ pageReference, replace });
23 }
24 [GenerateUrl](pageReference) {
25 mockGenerate({ pageReference });
26 return new Promise((resolve) => resolve('https://www.example.com'));
27 }
28 };
29};
30NavigationMixin.Navigate = Navigate;
31NavigationMixin.GenerateUrl = GenerateUrl;
32
33/*
34 * Tests do not have access to the internals of this mixin used by the
35 * component under test so save a reference to the arguments the Navigate method is
36 * invoked with and provide access with this function.
37 */
38export const getNavigateCalledWith = () => {
39 // If the mock was never called, return the object with undefined properties.
40 // This prevents exceptions when tests destructure this object, allowing them to
41 // fail in more expected ways as the test verifies properties on the pageReference object.
42 if (mockNavigate.mock.calls.length === 0) {
43 return {
44 pageReference: undefined,
45 replace: undefined
46 };
47 }
48
49 // The mock was called, so return the most recent last call.
50 // Because the mock is called with a single object, it's at the zero index.
51 return mockNavigate.mock.lastCall[0];
52};
53
54export const getGenerateUrlCalledWith = () => {
55 // If the mock was never called, return the object with undefined properties.
56 // This prevents exceptions when tests destructure this object, allowing them to
57 // fail in more expected ways as the test verifies properties on the pageReference object.
58 if (mockGenerate.mock.calls.length === 0) {
59 return {
60 pageReference: undefined
61 };
62 }
63
64 // The mock was called, so return the most recent last call.
65 // Because the mock is called with a single object, it's at the zero index.
66 return mockGenerate.mock.lastCall[0];
67};Use the mock with a jest.config.js config file.
1const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
2const setupFilesAfterEnv = jestConfig.setupFilesAfterEnv || [];
3setupFilesAfterEnv.push('<rootDir>/jest-sa11y-setup.js');
4module.exports = {
5 ...jestConfig,
6 moduleNameMapper: {
7 // Jest mocks
8 '^@salesforce/apex$': '<rootDir>/force-app/test/jest-mocks/apex',
9 '^@salesforce/schema$': '<rootDir>/force-app/test/jest-mocks/schema',
10 '^lightning/navigation$':
11 '<rootDir>/force-app/test/jest-mocks/lightning/navigation',
12 '^lightning/platformShowToastEvent$':
13 '<rootDir>/force-app/test/jest-mocks/lightning/platformShowToastEvent',
14 '^lightning/uiRecordApi$':
15 '<rootDir>/force-app/test/jest-mocks/lightning/uiRecordApi',
16 '^lightning/messageService$':
17 '<rootDir>/force-app/test/jest-mocks/lightning/messageService',
18 '^lightning/actions$':
19 '<rootDir>/force-app/test/jest-mocks/lightning/actions',
20 '^lightning/modal$':
21 '<rootDir>/force-app/test/jest-mocks/lightning/modal',
22 '^lightning/refresh$':
23 '<rootDir>/force-app/test/jest-mocks/lightning/refresh',
24 '^lightning/logger$':
25 '<rootDir>/force-app/test/jest-mocks/lightning/logger'
26 },
27 setupFiles: ['jest-canvas-mock'],
28 setupFilesAfterEnv,
29 testTimeout: 10000
30};When you call Apex methods imperatively, the Apex code doesn’t run in your Jest tests. You need to mock the Apex method to return test data.
To mock Apex methods, update your jest.config.js file to include a moduleNameMapper entry for @salesforce/apex. This entry tells Jest where to find your mock Apex methods.
1moduleNameMapper: {
2 '^@salesforce/apex$': '<rootDir>/force-app/test/jest-mocks/apex'
3}Let’s look at a component that creates a contact record by calling an Apex method imperatively.
1// contactCreator.js
2import { LightningElement } from 'lwc';
3import createContact from '@salesforce/apex/ContactController.createContact';
4
5export default class ContactCreator extends LightningElement {
6 async handleCreate() {
7 const result = await createContact({ lastName: 'Smith' });
8 this.contactId = result.id;
9 }
10}Create a mock Apex controller in force-app/test/jest-mocks/apex/ContactController.js.
1// ContactController.js
2export const createContact = jest.fn();Here’s the test for this component.
1// contactCreator.test.js
2import { createElement } from 'lwc';
3import ContactCreator from 'c/contactCreator';
4import createContact from '@salesforce/apex/ContactController.createContact';
5
6describe('c-contact-creator', () => {
7 afterEach(() => {
8 while (document.body.firstChild) {
9 document.body.removeChild(document.body.firstChild);
10 }
11 jest.clearAllMocks();
12 });
13
14 it('creates contact', async () => {
15 // Mock the Apex method to return an ID
16 const mockContactId = '003000000000001';
17 createContact.mockResolvedValue({ id: mockContactId });
18
19 // Create a component and call the method
20 const element = createElement('c-contact-creator', {
21 is: ContactCreator
22 });
23 document.body.appendChild(element);
24 element.handleCreate();
25
26 // Wait for async operation
27 await Promise.resolve();
28
29 // Verify Apex was called and component state updated
30 expect(createContact).toHaveBeenCalledWith({ lastName: 'Smith' });
31 expect(element.contactId).toBe(mockContactId);
32 });
33});Update and delete operations follow the same pattern. The only difference is the mock return value.
Update Operation
1// In the ContactController.js mock file
2export const updateContact = jest.fn();
3
4// In the contactCreator.test.js test file
5updateContact.mockResolvedValue({ success: true });Delete Operation
1// In the ContactController.js mock file
2export const deleteContact = jest.fn();
3
4// In the contactCreator.test.js test file
5deleteContact.mockResolvedValue();When the state of a Lightning web component changes, the DOM updates asynchronously. To ensure that your test waits for updates to complete before evaluating the result, return a resolved Promise. Chain the rest of your test code to the resolved Promise. Jest waits for the Promise chain to complete before ending the test. If the Promise ends in the rejected state, Jest fails the test.
1test("element does not have slds-icon class when bare", () => {
2 const element = createElement("one-primitive-icon", { is: PrimitiveIcon });
3 document.body.appendChild(element);
4 // Property value is assigned after the component is inserted into the DOM
5 element.variant = "bare";
6
7 // Use a promise to wait for asynchronous changes to the DOM
8 return Promise.resolve().then(() => {
9 expect(element.classList).not.toContain("slds-icon");
10 });
11});The appendChild() call inserts the component into the DOM and the lifecycle hooks connectedCallback() and renderedCallback() are called. The example then sets the element value after the appendChild() call, which resembles cases where properties are set by another method or user interaction after the component is inserted into the DOM. In these cases, use a promise to wait for the asynchronous DOM update. For more information on the component lifecycle and rendering, see Lifecycle Flow.
In cases where the property is set before the appendChild() call, the component is rendered synchronously. When the property is set before the appendChild() call, you don’t need to wait for asynchronous updates or return a promise.
See Also