Skip to main content

Write a Jest Test

Learning Objectives

After completing this unit, you'll be able to:

  • Write a test to verify your setup.
  • Write a failing test and change your component to make it pass.
  • Identify the basic Jest commands.
  • Explain the Lifecycle Hooks.

Start with a Lightning Web Component

To test a Lightning web component, we must first have a component to test.

Create a Lightning Web Component

  1. In Visual Studio Code, open the Command Palette by pressing Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS).
  2. Enter lightning web.
  3. Select SFDX: Create Lightning Web Component.
  4. Enter unitTest for the name of the new component.
  5. Press Enter.
  6. Press Enter again to accept the default force-app/main/default/lwc.

This creates the unitTest directory in the lwc directory with the initial base files.

unitTest directory in the test-lwc project.

Write a Basic Test

Jest tests are written, saved, and run differently than Jasmine or Mocha tests written for the Lightning Testing Service for Aura Components. Jest tests are local only and are saved and run independently of Salesforce. In fact, you'll get an error if you try to deploy Jest tests to your Salesforce org. Although Jest tests for Lightning web components are not deployed to your Salesforce org, be sure to commit them to version control along with the component itself.

The __tests__ Folder

The test files need to be separated out from the other component files. If one wasn't automatically created, create a folder named __tests__ at the top level of your component's bundle folder. Save all tests for this component inside the __tests__ folder.

  1. In Visual Studio Code, right-click the unitTest directory and select New Folder.
  2. Enter __tests__.
  3. Press Enter.

Configure .forceignore

Share tests with other team members or systems by committing the __tests__ folder to version control. They are a valuable part of your project and continuous integration process. To keep them from getting deployed to Salesforce the .forceignore file has an exclusion entered for it.

  • Ensure your project's .forceignore file contains the following exclusions. If not, add it and save the file.
    # LWC configuration files
    **/jsconfig.json
    **/.eslintrc.json
    # LWC Jest
    **/__tests__/**

Create a Jest Test File

Our first test is a simple one. We have a sum() function that is expected to add two numbers that are passed to it as arguments.

  1. In Visual Studio Code, right-click the __tests__ directory and select New File.
  2. Enter sum.test.js.
  3. Press Enter.
  4. Enter the following code into the new test file:
    import { sum } from '../sum';
        
    describe('sum()', () => {
      it('should add 1 and 2 returning 3', () => {
        expect(sum(1, 2)).toBe(3);
      });
    });
  5. Save the file.

Run the Test

  1. In Visual Studio Code, select View and then select Terminal. This opens a terminal in Visual Studio Code. The terminal defaults to the current project top-level directory.
  2. In the terminal, execute the following command from the prior unit:
    npm run test:unit
  3. The test fails due to the missing sum function.

Let's see how you fix it.

  1. In Visual Studio Code, right-click the unitTest directory and select New File.
  2. Enter sum.js.
  3. Press Enter.
  4. Enter the following code block into the new file:
    export function sum(x, y) {
      return x + y;
    }
  5. Save the file.
  6. In the terminal run the test again:
    npm run test:unit
  7. The test passes.

Congratulations! You just confirmed that Jest is set up and working.

Let's look at the test code to see what's happening.

import { sum } from '../sum';
    
describe('sum()', () => {
  it('should add 1 and 2 returning 3', () => {
    expect(sum(1, 2)).toBe(3);
  });
});
  • Line 1 imports the exported sum function from the sum JavaScript file.
  • Line 3 is the start of the Jest test suite. The describe function, or block, is a test suite and accepts two arguments. First is the description of the unit we are testing which is usually in the form of a noun. Second is a callback function that holds one or more tests. You can also nest describe test suites inside each other to create greater clarity.
  • Line 4 is the test (it is an alias for test). The it function, or block, accepts two arguments as well. First is another description of what we are expecting, usually starting with a verb. Then a callback function that builds up the test and holds the assertions or expectations for the test.
  • Line 5 is the expect statement asserting the success condition: that the sum function would add the two arguments, 1 and 2, and return 3. toBe is one of the many Jest matchers.
    Add another assertion with the following line right after line 5:
        expect(sum(1, 2)).not.toBeGreaterThan(3);
  • Adding .not and .toBeGreaterThan ensures that the number is not greater than 3. You could add another expect statement with .not.toBeLessThan(3).

Now for the Lightning web component test.

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.

Go through the process again with a unitTest test file

This test will verify that a property is set and when added to the DOM will display the correct text. The unitTest.test.js file was created with the __tests__ folder when the SFDX: Create Lightning Web Component command was run.

  1. Replace the code in unitTest.test.js with the following:
    import { createElement } from 'lwc';
    import UnitTest from 'c/unitTest';
      
    describe('c-unit-test', () => {
      afterEach(() => {
        // The jsdom instance is shared across test cases in a single file so reset the DOM
        while(document.body.firstChild) {
          document.body.removeChild(document.body.firstChild);
        }
      });
        
      it('displays unit status with default unitNumber', () => {
        const element = createElement('c-unit-test', {
          is: UnitTest
        });
        expect(element.unitNumber).toBe(5);
        // Add the element to the jsdom instance
        document.body.appendChild(element);
        // Verify displayed greeting
        const div = element.shadowRoot.querySelector('div');
        expect(div.textContent).toBe('Unit 5 alive!');
      });
    });
  2. Save the file.
  3. In the Terminal run the tests again:
    npm run test:unit
  4. The tests fail with this:
    Test Suites: 1 failed, 1 passed, 2 total
    Tests:       1 failed, 1 passed, 2 total

Let's look at that test code to see what the requirements are before updating the code to get a passing test.

  • Line 1 is new. It imports the createElement method from the lwc framework. It's only available in tests.
  • Line 2 imports the UnitTest class from the component JavaScript controller.
  • Line 4 starts the describe test suite block.
  • Line 5 is a Jest cleanup method. afterEach() is one of Jest's setup and cleanup methods. afterEach() runs after each test in the describe block it is in. This afterEach() method resets the DOM at the end of the test. Jest isn't running a browser when tests run. Jest uses jsdom to provide an environment that behaves much like a browser's DOM or document. Each test file gets a single instance of jsdom, and changes aren't reset between tests inside the file. It's a best practice to clean up between tests so that a test's output doesn't affect any other test. There are other setup and cleanup methods available. Check the Resources.
  • Line 12 starts the it test block.
  • Line 13 is where you use the imported createElement method. It creates an instance of the component and assigns it to the constant element.
  • Line 16 has the expect asserting the unitNumber variable is set to 5. This is the first requirement that we are testing for, that unitNumber is set to 5 first.
  • Line 18 actually adds the element to the jsdom's version of document.body using the appendChild method. The call attaches the Lightning web component to the DOM and renders it, which also means the lifecycle hooks connectedCallback() and renderedCallback() are called (more on these later).
  • Line 20 uses querySelector (a standard DOM query method) to search the DOM for a div tag. 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.
  • Finally, line 21 has the expect looking at the textContent of the div tag to assert 'Unit 5 alive!' is there. This is the final requirement. Asserting the text is correct.

To get the test to pass, you need to add code to the unitTest HTML and JavaScript files. We'll add code to fulfill the requirements.

  1. Click the unitTest.html file to open it.
  2. Overwrite unitTest.html with:
    <template>
      <lightning-card title="Unit Status" icon-name="standard:bot">
        <div class="slds-m-around_medium">
          Unit {unitNumber} alive!
        </div>
      </lightning-card>
    </template>
  3. Save the file.
  4. Click the unitTest.js file to open it and overwrite it with:
    import { LightningElement, api } from 'lwc';
    import { sum } from './sum';
      
    export default class UnitTest extends LightningElement {
      @api unitNumber = sum(2,3);
    }
  5. Save the file and run the tests:
    npm run test:unit
  6. All the tests pass.

Test Asynchronous DOM Updates

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. To do this, 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.

  1. Open unitTest.test.js.
  2. Add this second test after the last test.
    In this test, we want to verify that a property change will update the text in the DOM.
      it('displays unit status with updated unitNumber', () => {
        const element = createElement('c-unit-test', {
         is: UnitTest
        });
        // Add the element to the jsdom instance
        document.body.appendChild(element);
        // Update unitNumber after element is appended
        element.unitNumber = 6
          
        const div = element.shadowRoot.querySelector('div');
        // Verify displayed unit status
        expect(div.textContent).toBe('Unit 6 alive!');
      });
  3. Save the file and run the tests.
    npm run test:unit
  4. You get this fail message:
    Expected: "Unit 6 alive!"
    Received: "Unit 5 alive!"

What's going on? The expect statement is asserting that the div.textContext should be “Unit 6 alive” but it's still “Unit 5 alive!”. In order to see the change, we need to wait for it by returning a resolved Promise.

  1. Replace the failing expect statement with the following, right after the comment // Verify display unit status:
        expect(div.textContent).not.toBe('Unit 6 alive!');
        
        // Return a promise to wait for any asynchronous DOM updates. Jest
        // will automatically wait for the Promise chain to complete before
        // ending the test and fail the test if the promise rejects.
        return Promise.resolve().then(() => {
          expect(div.textContent).toBe('Unit 6 alive!');
        });
  2. Run the test using the same command as last time, or use one of the other options from the Run Jest Tests section of the previous unit.
  3. The test passes.

So far, so good. You have three successful tests in two test suites. Next, you add a fourth test so when an input field updates you can verify the unit status gets updated. To do this, use a change event in the input field.

  1. Open unitTest.test.js if it's not already open.
  2. Add a line after the last test you added and add this third test to the suite:
      it('displays unit status with input change event', () => {
        const element = createElement('c-unit-test', {
          is: UnitTest
        });
        document.body.appendChild(element);
        const div = element.shadowRoot.querySelector('div');
          
        // Trigger unit status input change
        const inputElement = element.shadowRoot.querySelector('lightning-input');
        inputElement.value = 7;
        inputElement.dispatchEvent(new CustomEvent('change'));
          
        return Promise.resolve().then(() => {
          expect(div.textContent).toBe('Unit 7 alive!');
        });
      });
  3. Save the file and run the test to see the failed message.
    Error message. You can see that only one test was run and the other two were skipped.

Let's look at what's being tested:

  • The first several lines should be familiar. You're adding the UnitTest to the document.body and then creating a reference to the div.
  • The constant inputElement is set with a reference to a lightning-input field.
  • Next, the value of that input field is set to 7.
  • Then we use dispatchEvent to trigger an event with a CustomEvent using an event type of “change”.
  • The Promise is familiar and only changed for the value of the changed input field.

Let's update the code to get it to pass. To do this, add the lightning-input to the HTML file and the handleChange method to the JavaScript controller.

  1. Open unitTest.html.
  2. Add the following code inside the lightning-card and before the div:
      <lightning-input
        label="Unit Number"
        value={unitNumber}
        onchange={handleChange} >
      </lightning-input>
  3. Save the file.
  4. Open unitTest.js.
  5. Add the following code after the @api unitNumber statement:
      handleChange(event) {
        this.unitNumber = event.target.value;
      }
  6. Save the file and run the tests.
  7. The tests pass because of the added input element and the JavaScript handler.

Test Lifecycle Hooks

Lightning web components have a lifecycle managed by the framework. The framework creates components, adds and removes them from the DOM, and renders DOM updates whenever the state of a component changes. There are several methods for interacting with the lifecycle.

The connectedCallback() lifecycle hook fires when a component is inserted into the DOM. The disconnectedCallback() lifecycle hook fires when a component is removed from the DOM. One use for these hooks is to register and unregister event listeners.

Take a look at code in the lmsSubscriberWebComponent from the lwc-recipes samples repo for a good example.

Next, we look at writing Jest tests for wire services.

Resources

Keep learning for
free!
Sign up for an account to continue.
What’s in it for you?
  • Get personalized recommendations for your career goals
  • Practice your skills with hands-on challenges and quizzes
  • Track and share your progress with employers
  • Connect to mentorship and career opportunities