Posted: November 16, 2022

Advanced LWC Jest Testing

Table of Contents:

  • Getting Started With Jest Unit Testing & The Rerendering Lifecycle

  • Thinking In (Terms Of) The Rendering Lifecycle

    • Simplifying Resolve Logic For Complicated Tests
    • Forcing A Full Rerender
  • Mocking External Dependencies

    • Testing Static Resource Loads
    • Polyfilling & Proxies
    • Lightning Web Security / Locker Service
  • Wrapping Up

Lightning Web Components (LWCs) have been around on the platform for five years, but I still see rudimentary questions about testing LWCs constantly. This article will attempt to start slow before deep-diving on interesting topics for those looking to level up their frontend unit testing chops. There’s a compelling rationale for becoming proficient with Jest and the @salesforce/sfdx-lwc-jest dependency — unit testing your components is an integral strategy for “shifting left” when it comes to the frontend; particularly given the fact that there’s no server necessary beyond your local computer to perform Jest unit testing, getting fast feedback and insights into how your components behave will allow you to more easily design for and avoid common edge cases and potential exception states in your components. Put in other words: when your unit tests can tell you whether or not your component “displays” properly, the feedback loop from design time to deploy time can be extremely short.

Getting Started With Jest Unit Testing & The Rerendering Lifecycle

This article begins with the assumption that you have this, at the very least, in your package.json file:

{
  "dependencies": {
    "@salesforce/sfdx-lwc-jest": "latest"
  }
}

That’s pretty much all we need to get started. Let’s dive right in to a simple example Lightning Web Component:

<template>
  <div>{greeting}</div>
</template>
import { api, LightningElement } from "lwc";

export default class Example extends LightningElement {
  @api
  greeting = "Hello world!";
}

So we have a default “greeting” that can display, and people looking to make use of this component can customize it as necessary. This seems like a pretty easy thing to test, and indeed when you use the VS Code command palette option for creating a new Lightning Web Component, a lot of the boilerplate is taken care of immediately:

import { createElement } from "lwc";
import Example from "c/example";

const mountElement = () => {
  const element = createElement("c-example", {
    is: Example,
  });

  document.body.appendChild(element);
  return element;
};

describe("c-example tests", () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it("shows how the rendering cycle happens asynchronously", () => {
    const element = mountElement();

    // we don't have to "re-query" this element, later -
    // its state will update as component rendering cycles complete
    const greetingDiv = element.shadowRoot.querySelector("div");

    expect(greetingDiv.textContent).toBe("Hello world!");

    element.greeting = "something else";

    expect(greetingDiv.textContent).toBe("something else");
  });
});

Note how I’ve abstracted out that mountElement function. Interestingly though, this test fails when run:

● c-example tests › shows how the rendering cycle happens asynchronously

  expect(received).toBe(expected) // Object.is equality

  Expected: "something else"
  Received: "Hello world!"

    34 |     element.greeting = 'something else';
    35 |
  > 36 |     expect(greetingDiv.textContent).toBe('something else');
        |                                     ^
    37 |   });

    at Object.toBe (core/lwc/example/__tests__/example.test.js:36:37)

Thus we are introduced to the rerendering lifecycle very early on when testing with Jest. Here’s what the docs have to say about this exact situation:

Component rerendering upon a property change is asynchronous, so the order in which something is added to the DOM isn’t always predictable. We recommend that your test waits for a value change to be reflected in the DOM before checking for the expected behavior.

What that means, practically, is that the test ends up looking something like this:

it("shows how the rendering cycle happens asynchronously", async () => {
  const element = mountElement();
  const greetingDiv = element.shadowRoot.querySelector("div");

  expect(greetingDiv.textContent).toBe("Hello world!");

  element.greeting = "something else";

  await Promise.resolve("wait for rendering cycle to update component HTML");

  expect(greetingDiv.textContent).toBe("something else");
});

A couple of things to note here:

  1. The function being passed to Jest’s it method (which names the test) has now been marked as async
  2. That allows us to use the await keyword in conjunction with a resolved promise
  3. Note the string passed to the Promise.resolve() call. This isn’t something that the docs show off; it’s something that my team and I have found to be enormously helpful to self-document component re-rendering lifecycles. More on this in a second — in the meantime, a big thanks to Jonathan Gillespie for coming up with this idea, which we as a team have eagerly embraced!

Thinking In (Terms Of) The Rendering Lifecycle

It’s quite common for components to rely on data passed from Apex — or from LWC helper methods like getRecord — to update markup state accordingly. As a simple example of this, we can update our example component JS without modifying the markup at all:

import { api, LightningElement } from "lwc";

export default class Example extends LightningElement {
  @api
  greeting = "Hello world!";
  @api
  tickFunction;

  renderedCallback() {
    console.log(`Re-rendering, current greeting value: ${this.greeting}`);
    this._pollTicker();
  }

  _pollTicker = () => {
    if (!this.tickFunction) {
      return;
    }
    this.tickFunction()?.then((result) => {
      if (result) {
        // purely for example. You shouldn't set bound properties
        // within renderedCallback() without a guard clause to prevent
        // infinite recursion
        this.greeting = result;
      } else {
        this._pollTicker();
      }
    });
  };
}

In this example, the _pollTicker function — which can be passed to the component as a public property — recursively calls itself until a new greeting value is resolved. Now, most polling functions take advantage of setTimeout or setInterval so that they aren’t recursively calling themselves more than is necessary, but this is a good starting point for the subject matter at hand: how Lightning Web Components respond to @api properties being updated, and how those updates effect the component rerendering lifecycle.

In the last example and first test, updating the greeting property and awaiting a resolved promise runs the component’s renderedCallback() function — that’s because renderedCallback() is invoked for every Lightning Web Component each time (as advertised) that it’s rendered or re-rendered. Straight from the docs, we can find some information as to which property updates typically cause a re-render to occur:

Although fields are reactive, the LWC engine tracks field value changes in a shallow fashion. Changes are detected when a new value is assigned to the field by comparing the value identity using ===. This works well for primitive types like numbers or boolean. … the framework doesn’t observe mutations made to complex objects, such as objects inheriting from Object, class instances, Date, Set, or Map.

A function is a complex object, and as such when we pass a value in for tickFunction within a new Jest unit test …:

it("shows how to mock async dependencies in jest", async () => {
  const weGotALiveOne = "we got a live one!";
  // we'll cover all of the wonderful merits of jest.fn()
  // in a bit
  const ticker = jest.fn();
  ticker.mockResolvedValueOnce(undefined);
  ticker.mockResolvedValueOnce(weGotALiveOne);
  const element = mountElement();
  // setting an @api decorated property SHOULD
  // trigger a re-render ... right?
  element.tickFunction = ticker;

  await Promise.resolve("first tick!");
  await Promise.resolve("second tick!");

  expect(element.greeting).toBe(weGotALiveOne);
  await Promise.resolve("re-rendering after having set greeting value");

  const greetingDiv = element.shadowRoot.querySelector("div");
  expect(greetingDiv.textContent).toBe(weGotALiveOne);
});

This is what we see in the output:

console.log
  Re-rendering, current greeting value: Hello world!

    at Example.log [as renderedCallback] (core/lwc/example/example.js:10:13)

FAIL  core/lwc/example/__tests__/example.test.js
c-example tests
  × shows how to mock async dependencies in jest (33 ms)
  ○ skipped shows how the rendering cycle happens asynchronously

● c-example tests › shows how to mock async dependencies in jest

  expect(received).toBe(expected) // Object.is equality

  Expected: "we got a live one!"
  Received: "Hello world!"

  > 53 |     expect(element.greeting).toBe(weGotALiveOne);
        |                              ^
    54 |     await Promise.resolve('re-rendering after having set greeting value');
    55 |
    56 |     const greetingDiv = element.shadowRoot.querySelector('div');

    at Object.toBe (core/lwc/example/__tests__/example.test.js:53:30)

What we can see, with that upper console.log statement, is that the renderedCallback() function has only been invoked once, when it seems like it should have been invoked twice:

  • once when tickFunction was set
  • once when the two ticks have finished and greeting has been updated by our “ticker”

To demonstrate why it’s crucial to “think in (terms of) the rendering cycle”, we can make a few simple changes to our test:

it("shows how to mock async dependencies in jest", async () => {
  const weGotALiveOne = "we got a live one!";
  const ticker = jest.fn();
  ticker.mockResolvedValueOnce(undefined);
  ticker.mockResolvedValueOnce(weGotALiveOne);
  const element = mountElement();
  element.tickFunction = ticker;
  element.greeting = "some other thing";
  await Promise.resolve("re-rendering from setting element.greeting");

  await Promise.resolve("first tick!");
  // the tickFunction has only been called once, here
  // so the public property has yet to be updated
  expect(element.greeting).toBe("some other thing");
  await Promise.resolve("second tick!");
  // at this point, element.greeting has the correct value
  // but the DOM does not
  await Promise.resolve("re-rendering after having set greeting value again");
  expect(element.greeting).toBe(weGotALiveOne);

  const greetingDiv = element.shadowRoot.querySelector("div");
  expect(greetingDiv.textContent).toBe(weGotALiveOne);
});

That updates our Jest test output to:

  console.log
    Re-rendering, current greeting value: Hello world!

      at Example.log [as renderedCallback] (core/lwc/example/example.js:10:13)

  console.log
    Re-rendering, current greeting value: some other thing

      at Example.log [as renderedCallback] (core/lwc/example/example.js:10:13)

  console.log
    Re-rendering, current greeting value: we got a live one!

      at Example.log [as renderedCallback] (core/lwc/example/example.js:10:13)

 PASS  core/lwc/example/__tests__/example.test.js
  c-example tests
    √ shows how to mock async dependencies in jest (47 ms)

Hopefully it’s starting to become clear: thinking in the rendering lifecycle is crucial to understanding when and how resolved promises are used throughout Jest testing. We, as a team, will often try adding a few more resolve statements when tests that we expect to “just work” don’t. In other words — we’re attempting to mutation test the question: “have updates to the component changed the component rendering lifecycle?”

Simplifying Resolve Logic For Complicated Tests

While composition is favored in LWC, sometimes our components end up having naturally complicated rendering lifecycles. Rather than creating many tests that all feature the same four or five self-documenting await Promise.resolve('awaiting reason')-type deals, we can go even further:

it("shows how to mock async dependencies in jest", async () => {
  const weGotALiveOne = "we got a live one!";
  const ticker = jest.fn();
  ticker.mockResolvedValueOnce(undefined);
  ticker.mockResolvedValueOnce(weGotALiveOne);
  const element = mountElement();
  element.tickFunction = ticker;
  element.greeting = "some other thing";
  await runRenderingLifecycle();

  expect(element.greeting).toBe(weGotALiveOne);
  const greetingDiv = element.shadowRoot.querySelector("div");
  expect(greetingDiv.textContent).toBe(weGotALiveOne);
});

const runRenderingLifecycle = async (
  resolveReasons = [
    "re-rendering from setting element.greeting",
    "first tick",
    "second tick",
    "re-rendering after having set greeting value again",
  ]
) => {
  while (resolveReasons.length > 0) {
    await Promise.resolve(resolveReasons.pop());
    runRenderingLifecycle(resolveReasons);
  }
};

Especially for tests that aren’t interested in testing out the intermediate state of any given component, but just the “end” state, creating a function that centralizes that self-documentation process while still allowing for flexibility can be an extremely powerful tool. It also allows us to re-write our original passing test as:

it("shows how to mock async dependencies in jest", async () => {
  const weGotALiveOne = "we got a live one!";
  const ticker = jest.fn();
  ticker.mockResolvedValueOnce(undefined);
  ticker.mockResolvedValueOnce(weGotALiveOne);
  const element = mountElement();
  element.tickFunction = ticker;
  element.greeting = "some other thing";
  // since there's only one rendering lifecycle getting resolved here
  // this could just as easily be a "self-documenting" Promise.resolve
  await runRenderingLifecycle(["re-render from setting greeting"]);

  expect(element.greeting).toBe("some other thing");
  await runRenderingLifecycle([
    "tick",
    "tick two",
    "re-rendering from having set greeting",
  ]);

  const greetingDiv = element.shadowRoot.querySelector("div");
  expect(greetingDiv.textContent).toBe(weGotALiveOne);
});

That way, when you need granular control over how many rendering cycles are processed, you have it, or you can simply call await runRenderingLifecycle() to move through the default list.

Forcing A Full Rerender

As shown above, testing the intermediate state of a component is an important part of the testing process, and our Jest tests are incomplete if we ignore how to incrementally move through various transitional states in our components. With that being said, you can always fall back on the following trick to force a component to resolve all pending promises and rendering cycles:

const forceFullRerendering = () =>
  new Promise((resolve) => setTimeout(resolve, 0));

it("shows how to fully flush promises in Jest", async () => {
  const weGotALiveOne = "we got a live one!";
  const ticker = jest.fn();
  ticker.mockResolvedValueOnce(undefined);
  ticker.mockResolvedValueOnce(weGotALiveOne);
  const element = mountElement();
  element.tickFunction = ticker;
  element.greeting = "some other thing";

  await forceFullRerendering();

  const greetingDiv = element.shadowRoot.querySelector("div");
  expect(greetingDiv.textContent).toBe(weGotALiveOne);
});

Many times in examples pulled from online, this function is called simply flushPromises — so long as the difference between this function versus a more incremental-rerendering approach is clear, all is well. You can think of this as the integration test version of a Jest test: not concerned with the intermediate details, just with the end result of how a component looks once all rerendering is complete.

Mocking External Dependencies

For this example, let’s assume we want to make use of an external Locker /Lightning Web Security compliant library that will be available to our component via a static resource. I’ll be using ChartJS because we use it extensively at work to make beautiful graphs — and also because, as is often the case with external dependencies, it comes with idiosyncracies that translate well to more advanced Jest testing concepts.

Testing Static Resource Loads

The bare minimum to load static resources requires a reference to your resource, as well as the loadScript function from the LWC base library:

import { loadScript } from "lightning/platformResourceLoader";
import chartJs from "@salesforce/resourceUrl/ChartJs";

// and then in a typical component ...
async renderedCallback() {
  await loadScript(this, chartJs);
}

Once the static resource has been loaded, it appends a new JavaScript class to the global Window object --- but if our LWC’s that want to use Chart JS intend to interact with window.Chart, we need to mock the creation of that object in our tests. When you have multiple Lightning Web Components that rely on the same test setup, you can centralize that test setup through the jest.config.js file, which sits within the root directory of your project:

const { jestConfig } = require("@salesforce/sfdx-lwc-jest/config");
module.exports = {
  ...jestConfig,
  moduleNameMapper: {
    "^lightning/platformResourceLoader":
      "<rootDir>/jest-mocks/lightning/platformResourceLoader",
  },
  coveragePathIgnorePatterns: ["/node_modules/"],
  setupFiles: ["jest-canvas-mock"],
};

That moduleNameMapper object is where we can direct our jest test setup to use something other than our base dependency on the resource loader. Idiomatically, overrides like this tend to live within a jest-mocks folder, with subdirectories that map to the dependency you’re mocking — in our example mapping, you can see that amounts to ./jest-mocks/lightning/, with a file called platformResourceLoader.js. Here’s where we standardize what happens within loadScript when running our Jest unit tests:

export const mockChartImplementation = jest.fn().mockImplementation(() => {
  // not necessarily something we'll be going further into here
  // but this is to show that if you need to call functions on your dependencies
  // this is how you can add those mock signatures
  return { destroy: jest.fn(), resize: jest.fn() };
});

export const loadScript = () => {
  return new Promise((resolve, _) => {
    global.window = {};
    global.window.Chart = mockChartImplementation;
    resolve();
  });
};

Now, all tests for components that are going to make use of Chart JS are free to interact with the window.Chart object without errors being thrown:

<template>
  <!-- simplest possible markup required by Chart JS -->
  <canvas class="chart" lwc:dom="manual"></canvas>
</template>
// somewhere in your LWC code
// we don't actually use the context object, but it's required by the Chart constructor
const ctx = this.template.querySelector("canvas.chart").getContext("2d");
const config = { data: {}, options: { responsive: true } };
//  do something with the chart
new window.Chart(ctx, config);

Polyfilling & Proxies

In the above example, passing an options object to Chart JS’s config with the responsive property set to true makes use of ResizeObserver. Now, when a LWC is being mounted to the DOM, the order of operations as to how web APIs like ResizeObserver are made available isn’t always the same. This is particularly true when a user hits the back button on their browser, and cached versions of components load before the rest of the page. All of that is to say, we now have an unrepresented dependency to ResizeObserver — and we can test for that!

import { createElement } from "lwc";
// use better names than this
import Chart from "c/chart";

describe("c-chart", () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
    jest.clearAllMocks();
    if (global.window.ResizeObserver) {
      delete global.window.ResizeObserver;
    }
  });

  it("safely mounts component", async () => {
    const element = createElement("c-chart", {
      is: Chart,
    });

    document.body.appendChild(element);

    // again, this is the bare minimum markup required by ChartJS
    expect(element.shadowRoot.querySelector("canvas")).toBeTruthy();
    expect(global.window.ResizeObserver).toBeTruthy();
    // validate observe can be called as function
    expect(typeof new global.window.ResizeObserver().observe).toBe("function");
  });
});

Assuming we already have the canvas markup shown previously, we can get this test to pass by creating a ResizeObserver polyfill:

// in c-chart
connectedCallback() {
  if (!window.ResizeObserver) {
    window.ResizeObserver = class {
      observe = function () {};
    };
  }
}

This is the simplest possible polyfill; a more complicated polyfill is actually more of a delegate, or Proxy. For example, I’ve worked on several projects where Lightning Web Components used libraries like Google Analytics, Tealium, LogRocket, etc … in each of these cases, there’s a routine pattern to how you interact with the library:

  • load it as an external dependency with loadScript
  • wait for it to finish loading
  • perform some kind of initialization ceremony on the newly loaded object — this frequently takes the form of passing some kind of secret credential onwards, thus establishing connection with your company’s account in the external dependency
  • start sending messages to the dependency as interactions occur on the page

Indeed, one of the very first thing one sees on the LogRocket JS SDK documentation page is the following warning:

Call “init” as early on the page as possible. Some recording data may be lost if it is called too late.

That’s the sort of situation that, for any dependency (external or otherwise), we’d really like to avoid. Using LogRocket as an example:

import { api, LightningElement } from "lwc";
import { loadScript } from "lightning/platformResourceLoader";
import logRocket from "@salesforce/resourceUrl/LogRocket";

export default class LogRocketDispatcher extends LightningElement {
  @api
  logBuffer = [];
  _isLoaded = false;

  async renderedCallback() {
    if (!this._isLoaded) {
      await loadScript(logRocket);
      this._isLoaded = true;
      window.LogRocket.init("your app id");
    }
  }

  @api
  log(loggingLevel, ...args) {
    if (this._isLoaded) {
      window.LogRocket[loggingLevel](...args);
    }
  }
}

Ignoring the ugliness (for now, at least), this is the kind of compromise that we’d like to avoid — in return for safety, we lose the ability to actually interact with our dependency through the log function until some vague “future” time. We can write a test that demonstrates the issue:

import { createElement } from "lwc";
import { loadScript } from "lightning/platformResourceLoader";
import LogRocketDispatcher from "c/logRocketDispatcher";

jest.mock(
  "lightning/platformResourceLoader",
  () => ({ loadScript: jest.fn() }),
  {
    virtual: true,
  }
);

describe("c-log-dispatcher", () => {
  afterEach(() => {
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
    jest.clearAllMocks();
  });

  it("logs buffered messages successfully", async () => {
    loadScript.mockResolvedValueOnce(
      new Promise((resolve) => {
        // LogRocket actually has ALL the logging functions
        // available on window.console, but we will keep this simple
        global.window.LogRocket = { log: jest.fn(), init: jest.fn() };
        resolve();
      })
    );
    const element = createElement("c-log-rocket-dispatcher", {
      is: LogRocketDispatcher,
    });

    document.body.appendChild(element);

    await Promise.resolve("loadScript promise");

    // we'll prove to the world we too can accept a dynamic number of arguments!
    element.log("log", "hello world!", "again!");

    expect(element.logBuffer.length).toBe(1);

    await Promise.resolve("setTimeout resolution");
    await Promise.resolve(
      "re-render for logRockerInitialized flag being reset"
    );
    await Promise.resolve("re-render because log buffer has now been cleared");

    expect(global.window.LogRocket.init).toHaveBeenCalled();
    expect(global.window.LogRocket.log).toHaveBeenCalled();
    expect(global.window.LogRocket.log.mock.calls[0]).toEqual([
      "hello world!",
      "again!",
    ]);
    expect(element.logBuffer.length).toBe(0);
  });
});

Of course, this initially fails because we aren’t adding anything to our logBuffer attribute (this is one of those areas where you probably want an @api decorated getter which returns a copy of the list, since there’s no “test visible” property concept within Jest):

import { api, LightningElement } from "lwc";
import { loadScript } from "lightning/platformResourceLoader";
import logRocket from "@salesforce/resourceUrl/LogRocket";

export default class LogRocketDispatcher extends LightningElement {
  _logBuffer = [];

  @api
  get logBuffer() {
    return [...this._logBuffer];
  }
  _isLoaded = false;
  _logRocketInitialized = false;

  async renderedCallback() {
    if (!this._isLoaded) {
      await loadScript(logRocket);
      this._isLoaded = true;
    }
    if (this._logRocketInitialized === false && window.LogRocket) {
      this._logRocketInitialized = true;
      // you could also store window.LogRocket in a backing variable
      window.LogRocket.init("your app id");
    }
    if (this._isLoaded && this._logRocketInitialized) {
      this.logBuffer.forEach(({ loggingLevel, args }) => {
        this.log(loggingLevel, ...args);
      });
      this._logBuffer = [];
    }
  }

  @api
  log(loggingLevel, ...args) {
    if (this._isLoaded && this._logRocketInitialized) {
      window.LogRocket[loggingLevel](...args);
      // typically you'd gate something like this behind a feature flag
      // and if you run eslint on your projects it will CERTAINLY try to
      // convince you this line is a bad idea.
      console[loggingLevel](...args);
    } else {
      this._logBuffer.push({ loggingLevel, args });
    }
  }
}

And now the test passes. Note that local file mocks — like the one for lightning/platformResourceLoader that we’re using to demonstrate the LogRocket proxy, override the mocks defined in something like your jest.config.js. This makes for really nice, granular, control over how any component being tested can override “global” mocking while still allowing for shared groups of components to not have to repeat themselves.

Note, as well, how useful this block is:

await Promise.resolve("setTimeout resolution");
await Promise.resolve("re-render for logRockerInitialized flag being reset");
await Promise.resolve("re-render because log buffer has now been cleared");

Of course that can be combined with the runRenderingLifecycle function mentioned previously.

Lightning Web Security / Locker Service

Something to be especially mindful of when working with 3rd party libraries — particularly where large data volumes are being used (like in a charting library!) — is the component-level boundaries within Lightning Web Components. Because data passed between components is wrapped in a Proxy object by default, children-level components can make use of parent-level data but have to “clone” that data if they want to modify it:

// in <c-parent> markup
<template>
  <c-child data={data}></c-child>
</template>

If <c-child> is simply displaying different things depending on the data prop that’s being passed in, all is well and good. If, however, <c-child> passes data to a third party library that modifies any part of that object, or needs to modify the object itself, it has to do the following:

// in child.js
@api
data;

connectedCallback() {
  // first unproxify the data to prevent immutable errors
  this.data = JSON.parse(JSON.stringify(this.data));
}

Notes on this:

  • only complex properties (JavaScript objects) are proxified; things like numbers and strings will not be proxified across component boundaries
  • for relatively simple objects like { foo: { bar: 1 } }, the performance hit for re-copying those objects in memory is negligible; you might never even notice

We ran into this issue as a team, recently, while creating a subcomponent for charting. Because we were passing several thousand data points from the parent component to the child, and because the child had to unproxify the data in order for the charting library to successfully mutate it, the performance of the two components was unacceptable. There are various strategies to help with abstracting logic like this — the easiest of which is simply exposing a helper function used across components that need to transform data into the same shape. Remember that it’s 100% valid for a “LWC” to simply be a JS file exporting named constants:

// in chartUtils.js, for example
export const transformChartingData = (data) => {
  // do stuff with the data. You might even return a function here
  // if the transformed data isn't being used till later
}

// then in parent.js
import { transformChartingData } from "c/chartUtils"

// ... later in the file, instead of passing the data to a <c-child>

someActionThatCausesCharting() {
  transformChartingData(data);
  //  etc ...
}

Then, in tests, we can override the helper function (if necessary) through a variety of means. Here’s a simple example:

import { createElement } from "lwc";
import MyComponent from "c/myComponent";

let wasCalled = false;

const transformSpy = {
  wasCalled: false,
  transformChartingData: (data) => (wasCalled = true),
};

jest.mock("c/chartUtils", () => {
  return transformSpy;
});

describe("c-my-component", () => {
  afterEach(() => {
    wasCalled = false;
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it("Overrides helper function", async () => {
    const element = createElement("c-my-component", {
      is: MyComponent,
    });

    document.body.appendChild(element);
    await Promise.resolve();

    expect(wasCalled).toBeTruthy();
  });
});

Note that within this simple example, data isn’t even used in the transformSpy — this is just to show how to override helper functions in Jest, and how to clean up whatever kind of side-effects you do want to make within the afterEach Jest lifecycle function.

Wrapping Up

Once more, thinking in the component rendering lifecycle often helps to easily triage and fix simple issues — many of which are routinely encountered while writing Jest unit tests. I feel very grateful to have had Jest testing experience prior to Lightning Web Components having been released; “rendering lifecycles” as a phrase can already have the effect of glazing eyes over, and (in my experience) people will start to lose interest in testing when it feels like they’re “fighting the framework” to get work done.

Hopefully this article helps to illuminate some of the more advanced testing situations you can expect to uncover while working with Jest unit tests for LWC, and (if nothing else) serves as a crucial building block between the relatively simple documentation examples and something more geared towards components built and used in the real world. As always, thanks for reading — till next time!

In the past three years, a hundred thousand of you have come to read & enjoy the Joys Of Apex. Over that time period, I've remained staunchly opposed to advertising on the site, but I've made a Patreon account in the event that you'd like to show your support there. Know that the content here will always remain free. Thanks again for reading — see you next time!

Written by James Simone, Lead Engineer @ Salesforce, climber, and sourdough bread baker. For more shenanigans, check out She & Jim!

© 2019 - 2022, James Simone LLC