Salesforce

Testing Lightning Web Components

Testing Lightning Web Components

For anyone with experience of Aura component development, they’ve probably faced the issue of how to test the damned things! They were so heavily tied to the platform that they couldn’t easily be tested in isolation.

Fortunately Lightning Web Components has true TDD built in from the outset. It’s super simple to write tests and make assertions. Even better, if you’re used to writing tests with Jasmine, Mocha or Jest then you’ll be right at home.

Setting up your project

If you don’t yet have an LWC project I’d suggest following my previous post. In this post we’ll take what we did in that project and evolve to add some simple unit tests that we can run right from the command line on your computer… no need to upload code to the org!

Firstly we’ll need to add a project.json file to the root of our project. If you don’t have a project.json, just run npm init and follow the walkthrough – most options can be defaulted. After running this command you’ll need to add our test dependencies. On the command line run:

npm i -D jest @lwc/jest-preset @lwc/module-resolver @lwc/compiler @lwc/engine

This will install Jest (a popular testing framework from Facebook used heavily on React) and Salesforce LWC Jest integrations, including the compiler and engine. Now open your package.json and under the "scripts" object add a "test:unit" property who’s value is simply "jest".

  "main": "index.js",
  "scripts": {
    "test:unit": "jest"
  },
  "author": "",

The final step is to add a jest.config.js file to the root of our project. The contents should simply look like:

module.exports = {
    preset: '@lwc/jest-preset',
    moduleNameMapper: {
        '^(c)/(.+)$': '<rootDir>/force-app/main/default/lwc/$2/$2'
    },
    testPathIgnorePatterns: [
        '<rootDir>/node_modules/',
        '<rootDir>/test/specs/'
    ]
};

This is mostly boilerplate and in fact Salesforce has a simpler option which is documented in the Salesforce Developer Guide: https://developer.salesforce.com/docs/component-library/documentation/lwc/lwc.unit_testing_using_jest_installation

With this setup you can run your tests on the command line by typing:

$ npm run test:unit
No tests found
In /Users/matthewgoldspink/Desktop/MyFirstLWCProject
  5 files checked.
  testMatch: **/__tests__/**/?(*.)(spec|test).js - 0 matches
  testPathIgnorePatterns: /Users/matthewgoldspink/Desktop/MyFirstLWCProject/node_modules/,/Users/matthewgoldspink/Desktop/MyFirstLWCProject/test/specs/ - 5 matches
Pattern:  - 0 matches

As you can see we don’t have any tests yet, so let’s go add one.

BUT WAIT! There’s a simpler way

We just saw a lot of configuration – jest.config.js files and multiple npm installs. I showed the above to reinforce that this is just standard Open Source Jest tooling – nothing Salesforce proprietary! However that doesn’t mean that the guys at Salesforce don’t want to make things easier, and they have! You can actually replace everything we just installed with one simple npm command:

npm i -D jest @salesforce/lwc-jest

This will install everything we installed last time but behind a lwc-jest wrapper command. You can safely delete references to @lwc/jest-preset @lwc/module-resolver @lwc/compiler @lwc/engine in your package.json.

Next you can delete your jest.config.js and update your package.json scripts to be:

  "main": "index.js",
  "scripts": {
    "test:unit": "lwc-jest"
  },
  "author": "",

Now when you run npm run test:unit it will run the lwc-jest cli command which in turn will call jest with everything installed and configured for you!

Adding a test to an LWC Component

Each component can contain it’s own suite of tests, navigate to your component, in my project I’ll be working with a very simple helloComponent whose content is simply:

<template>
    <div class="slds-m-around_medium">
        <p>Hello, {person}!</p>
    </div>
</template>
import { LightningElement, api } from 'lwc';

export default class HelloComponent extends LightningElement {

    @api person = 'World';

}

Add a folder called __tests__. Now create a file called helloComponent.test.js:

Where to put your __tests__ folder for your lightning components.

Inside this file we’ll add our first test:

import { createElement } from 'lwc';
import HelloComponent from 'c/helloComponent';

describe('c-hello-component', () => {
    
    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);
        }
    });

    describe('Renders with Hello World', () => {
        const element = createElement('c-hello-component', {
            is: HelloComponent,
        });
        document.body.appendChild(element);

        const pTag = element.shadowRoot.querySelector('p');
        expect(pTag.textContent).toEqual('Hello, World!');
    });

});

There’s a lot here so let me try describe it in chunks:

import { createElement } from 'lwc';
import HelloComponent from 'c/helloComponent';

These first 2 lines import the createElement function from the lwc framework. This is the magic which knows how to take an LWC class and turn it into an HTML Element. We’ll use this further down. The second line is how we import our component that we want to test. The c/ prefix is simply the namespace of it – in Salesforce orgs the default namespace is usually c.

describe('c-hello-component', () => {
    
    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);
        }
    });

Next we add our first describe, this is essentially the wrapper around where our tests live. We also add an afterEach hook. The code here will be run after every test and as stated in the comment it basically resets the DOM. In other words, it will remove any LWC elements we rendered after each test so that the next test has a clean slate.

    describe('Renders with Hello World', () => {
        const element = createElement('c-hello-component', {
            is: HelloComponent,
        });
        document.body.appendChild(element);

        const pTag = element.shadowRoot.querySelector('p');
        expect(pTag.textContent).toEqual('Hello, World!');
    });

});

Finally our test! We use the createElement from the lwc import earlier to turn our HelloComponent into a real DOM element. Then we insert it into the body of our page. And finally we run our assertion – in this case I expect that there should be a <p> tag whose text content is Hello, World. If you re-run npm run test:unit you’ll get a nice happy test!

$ npm run test:unit
 PASS  force-app/main/default/lwc/helloComponent/__tests__/helloComponent.test.js
  c-hello-component
    ✓ Renders with Hello World (25ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.508s

Testing a property change

Whilst a passing test is nice, it’s not really covering all our logic. One thing we should do is verify that if we set the person property to a different value, then that value is reflected in the DOM too. Let’s add a new test for that. After your existing test add the following:

    it('Renders with Hello Matt', () => {
        const element = createElement('c-hello-component', {
            is: HelloComponent,
        });
        element.person = "Matt"
        document.body.appendChild(element);

        const pTag = element.shadowRoot.querySelector('p');
        expect(pTag.textContent).toEqual('Hello, Matt!');
    });

This test is pretty much the same as before except we’re now changing the value of person on the element to Matt and we’re updating our expectation to verify it now says Hello, Matt! instead. Let’s run it and see what happens:

$ npm run test

> myfirstlwcproject@1.0.0 test /Users/matthewgoldspink/Desktop/MyFirstLWCProject
> jest

 PASS  force-app/main/default/lwc/helloComponent/__tests__/helloComponent.test.js
  c-hello-component
    ✓ Renders with Hello World (25ms)
    ✓ Renders with Hello Matt (3ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.678s
Ran all test suites.

Hurray – It works!

Testing a property change Part 2

Now I want to show something that will likely catch a lot of people out. Let’s take the same test as we just did, but we’re going to move one line:

    it('Renders with Hello Matt', () => {
        const element = createElement('c-hello-component', {
            is: HelloComponent,
        });
        document.body.appendChild(element);

        element.person = "Matt"

        const pTag = element.shadowRoot.querySelector('p');
        expect(pTag.textContent).toEqual('Hello, Matt!');
    });

I’ve shifted the element.person = "Matt" line to be run after the element has been inserted into the DOM. If you run the tests now you should see:

$ npm run test

> myfirstlwcproject@1.0.0 test /Users/matthewgoldspink/Desktop/MyFirstLWCProject
> jest

 FAIL  force-app/main/default/lwc/helloComponent/__tests__/helloComponent.test.js
  c-hello-component
    ✓ Renders with Hello World (33ms)
    ✕ Renders with Hello Matt (13ms)

  ● c-hello-component › Renders with Hello Matt

    expect(received).toEqual(expected)

    Expected value to equal:
      "Hello, Matt!"
    Received:
      "Hello, World!"

      31 |
      32 |         const pTag = element.shadowRoot.querySelector('p');
    > 33 |         expect(pTag.textContent).toEqual('Hello, Matt!');
         |         ^
      34 |     });
      35 |
      36 | });

      at Object.expect (force-app/main/default/lwc/helloComponent/__tests__/helloComponent.test.js:33:9)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.251s
Ran all test suites.

Oh man! It fails!!! How can moving one line break a test?

The difference between the two versions of this test are that in the first version of the test we set the value of person before we inserted the element into the DOM, therefore when the element is inserted it has the value of Matt and renders it immediately. In the second version we set the value of person after the element has been inserted. This means that the component needs to be re-rendered in order to update the DOM… and rendering is asynchronous. So when we do our expectation immediately after setting the new value, the DOM hasn’t been re-rendered and so we still see Hello, World!.

So how do we fix this up? We need to run our expectations after then DOM has been updated. Fortunately there’s a nice easy way to do it… use a Promise. Let’s see what this updated test looks like:

    it('Renders with Hello Matt', () => {
        const element = createElement('c-hello-component', {
            is: HelloComponent,
        });
        document.body.appendChild(element);

        element.person = "Matt"

        return Promise.resolve().then(() => {
            const pTag = element.shadowRoot.querySelector('p');
            expect(pTag.textContent).toEqual('Hello, Matt!');
        });
    });

The change above basically creates an immediately resolving Promise and then runs our assertions on the next tick of the browser. If you run the tests now you should see 2 passing tests:

$ npm run test

> myfirstlwcproject@1.0.0 test /Users/matthewgoldspink/Desktop/MyFirstLWCProject
> jest

 PASS  force-app/main/default/lwc/helloComponent/__tests__/helloComponent.test.js
  c-hello-component
    ✓ Renders with Hello World (25ms)
    ✓ Renders with Hello Matt (3ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.277s, estimated 3s
Ran all test suites.

https://github.com/mattgoldspink/testing-lightning-web-components

All the code from this post is available as a github project for you to clone and run.

Next steps

There’s a lot more things we can do with our tests which I’ll cover in future posts including:

  • Triggering, capturing and asserting events
  • Mocking of Salesforce’s Lightning components
  • Mocking of data sources

I’d highly recommend reading some of Salesforce’s own example tests like in the e-bikes app. Here’s some additional links for more information:

Author: Matt Goldspink

I'm a web developer based in the UK. I'm currently UI Architect at Vlocity, Inc, developing UI's on the Salesforce platform using a mix of Web Components, Angular.js and Lightning.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.