Javascript Unit Testing
Jason Sun
Posted on February 16, 2021
(Heroku and Salesforce - From Idea to App, Part 10)
This is the 10th article documenting what I’ve learned from a series of 13 Trailhead Live video sessions on Modern App Development on Salesforce and Heroku. In these articles, we’re focusing on how to combine Salesforce with Heroku to build an “eCars” app—a sales and service application for a fictitious electric car company (“Pulsar”) that allows users to customize and buy cars, service techs to view live diagnostic info from the car, and more. In case you missed my previous articles, you can find the links to them below.
Modern App Development on Salesforce and Heroku
Jumping into Heroku Development
Data Modeling in Salesforce and Heroku Data Services
Building Front-End App Experiences with Clicks, Not Code
Custom App Experiences with Lightning Web Components
Lightning Web Components, Events and Lightning Message Service
Automating Business Processes Using Salesforce Flows and APEX
Scale Salesforce Apps Using Microservices on Heroku
More Scaling Salesforce Apps Using Heroku Microservices
Just as a quick reminder: I’ve been following this Trailhead Live video series to brush up and stay current on the latest app development trends on these platforms that are key for my career and business. I’ll be sharing each step for building the app, what I’ve learned, and my thoughts from each session. These series reviews are both for my own edification as well as for others who might benefit from this content.
The Trailhead Live sessions and schedule can be found here:
https://trailhead.salesforce.com/live
The Trailhead Live sessions I’m writing about can also be found at the links below:
https://trailhead.salesforce.com/live/videos/a2r3k000001n2Jj/modern-app-development-on-salesforce
https://www.youtube.com/playlist?list=PLgIMQe2PKPSK7myo5smEv2ZtHbnn7HyHI
Last Time…
Last time we dove deeper into connecting services and apps hosted on Heroku with Salesforce to provide real-time notifications and IoT data to Salesforce as well as how to scale these services for massive throughput needs. This time we’re looking at automated ways to verify that our Javascript apps and services code are running correctly. Specifically, we are going to learn about Javascript Unit Testing.
Automated unit testing is an important part of the application development process and a key component to making sure that code is running and behaving as expected. Unlike with Salesforce APEX code, which requires at least 75% overall code coverage of triggers and classes, Javascript apps don’t explicitly require unit tests, but it is simply good development practice to have unit tests as part of the process. Applications that try to cut corners on this aspect of the application development lifestyle end up paying the price in the form of discovering more expensive bugs during regression testing or worse, shipping apps with bugs to customers and end-users.
A Primer on Unit Testing
What is “unit testing” exactly and what’s different about unit testing versus another testing methodology like regression testing or smoke testing?
- The “unit” part refers to verifying that a discrete method in the code, functionality, or automation is working as intended separately from other dependencies. So, in Javascript, if you have a particular function that is supposed to take some arguments, process them, then fire a callback, you might write unit tests that target that specific function as opposed to testing the entire class.
- The “testing” part simply refers to ensuring the method in code, functionality or automation is working as expected.
Here are some examples of how unit tests can be valuable:
- Identify bugs easily and earlier
- Reduce costs related to fixing bugs and QA efforts
- Facilitate code design and refactoring
- Self-documents sets of test cases
However, these benefits are only fully realized after implementing unit testing thoughtfully and correctly. This process is often implemented poorly or skipped entirely.
A Framework for Unit Testing
Regardless of the programming language, we’re using, a good framework to base our unit test design is the Arrange, Act, and Assert (AAA) framework.
A good thing to remember with unit testing is that you are managing the “expectations” or “specifications” of the code with unit tests. This means the unit tests have something to say about code specs and what the expectations are for the code when it runs. In some cases, I’ve heard of development methods that involve writing the unit tests _first, _and then developing the actual code later since the unit tests can act as documentation for how the code should behave.
Some Javascript examples for can include:
- Arrange: Test data setup and inserting the necessary objects/records for the unit test
- Act: Call your methods/functions or otherwise run the code you want to test and capture the result
- Assert: Create assertion statements in your unit test and compare the results you captured with expected results. You want the test to error out or notify you in some way if the result you captured deviates from the expected result.
Jest – Testing Tool for Javascript and LWC
Javascript has a number of different testing libraries and frameworks available, but we’re focusing on one in particular: Jest. This is the recommended testing framework for Salesforce Lightning Web Components (LWC) because of its ease-of-use, open-source nature, and popularity. Specifically for LWC, Salesforce has a wrapper library on top of Jest called “sfdc-lwc-jest” which can be found at the following Github repo.
https://github.com/salesforce/sfdx-lwc-jest
Also if you’re using Visual Studio Code (VS Code) with LWC (and you should), you will also want to install the following extension packs for VS Code.
Unit Testing for the eCars App
The Javascript LWC apps and microservices hosted on the Heroku side of our app have a decent number of client-side interactions through the UI. Specifically, we are going to test the Car Configurator app. It makes sense to apply unit testing to our app so that when we make enhancements and additions to the code, our unit tests can help us catch issues with the app.
So many buttons and interactions, so many ways errors can happen
The Javascript car configurator app we are going to test can be found on the eCars app Github repository. If you’ve been following along with this series, then you should have already done some previous setup work installing the repo in VS Code and deploying it to a Salesforce scratch org.
If you look at the .html and .js file in the carConfigurator LWC, you’ll see that the HTML has some expected behavior and bindings to variables in the .js file. For our tests, we’re going to test that the LWC renders the expected components and values to the DOM as the test progresses. We’ll soon look at how Jest and the sfdc-lwc-jest wrapper library makes this magic happen.
First, we will need to create a testing scaffold for our unit tests with the following command in VS Code:
sfdx force:lightning:lwc:test:create -f [file path]
If the command runs successfully, your terminal should look something like this:
This creates a carConfigurator.test.js test scaffold file that we can use to build our tests. There should be a pre-built stub for the functions in the test file that you would expand upon as you build out your tests.
When you initialize your LWC project in VS Code and take a look at the package.json file, you’ll see that there are some lwc-dependencies and scripts that are included in the package. These scripts help make some of the lwc-jest magic happen.
Some lwc-jest dependencies called out in the package.json file
Unit Tests Code in Detail
There’s a lot to unpack if you’re like me and have never done Javascript unit testing or worked with the Jest library before. So, we will skip ahead to the fully built-out solution that is used in the demo and pick out some important things to note. Here’s the fully completed carConfigurator.test.js code below. In each of the test methods, you can get an idea of what the unit tests are testing for by looking at the name/description right after the it()
function declaration. We’ll review some of the important highlights in this test script after you’ve scrolled through.
import { createElement } from "lwc";
import CarConfigurator from "c/carConfigurator";
import CURRENCY from "@salesforce/i18n/currency";
import invokePdfCreateService from "@salesforce/apex/PdfCreateService.invokePdfCreateService";
jest.mock(
"@salesforce/apex/PdfCreateService.invokePdfCreateService",
() => {
return {
default: jest.fn(),
};
},
{ virtual: true }
);
describe("c-car-configurator", () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
// Helper function to wait until the microtask queue is empty. This is needed for promise
// timing when calling imperative Apex.
function flushPromises() {
// eslint-disable-next-line no-undef
return new Promise((resolve) => setImmediate(resolve));
}
it("renders section 1 with image", () => {
const element = createElement("c-car-configurator", {
is: CarConfigurator,
});
document.body.appendChild(element);
const imageDiv = element.shadowRoot.querySelector("img");
const rangeAnchors = element.shadowRoot.querySelectorAll("a");
const formattedNumbers = element.shadowRoot.querySelectorAll(
"lightning-formatted-number"
);
return Promise.resolve().then(() => {
expect(imageDiv).not.toBeNull();
expect(imageDiv.src).toBe(
"https://sfdc-demo.s3-us-west-1.amazonaws.com/ecars/car_white.jpg"
);
expect(rangeAnchors.length).toBe(3);
expect(rangeAnchors[0].dataset.range).toBe("Short Range");
expect(rangeAnchors[1].dataset.range).toBe("Medium Range");
expect(rangeAnchors[2].dataset.range).toBe("Long Range");
expect(formattedNumbers.length).toBe(3);
expect(formattedNumbers[0].currencyCode).toBe(CURRENCY);
expect(formattedNumbers[0].value).toBe(25000);
expect(formattedNumbers[1].value).toBe(35000);
expect(formattedNumbers[2].value).toBe(45000);
});
});
it("navigates to different section on next button click", () => {
const element = createElement("c-car-configurator", {
is: CarConfigurator,
});
document.body.appendChild(element);
const button = element.shadowRoot.querySelector("lightning-button");
button.click();
return Promise.resolve().then(() => {
const exteriorColorAnchors = element.shadowRoot.querySelectorAll("a");
expect(exteriorColorAnchors.length).toBe(5);
expect(exteriorColorAnchors[0].dataset.color).toBe("white");
expect(exteriorColorAnchors[1].dataset.color).toBe("black");
expect(exteriorColorAnchors[2].dataset.color).toBe("red");
expect(exteriorColorAnchors[3].dataset.color).toBe("blue");
expect(exteriorColorAnchors[4].dataset.color).toBe("green");
});
});
it("invokes pdf processing service", () => {
invokePdfCreateService.mockResolvedValue(true);
const LEADID = "00Q9A000001TNllUAG";
const INPUT_PARAMETERS = {
price: 25000,
range: "Short Range",
exteriorColor: "Pearl White",
interiorColor: "Vegan White",
leadRecordId: LEADID,
};
const element = createElement("c-car-configurator", {
is: CarConfigurator,
});
document.body.appendChild(element);
const button = element.shadowRoot.querySelector("lightning-button");
button.click();
const section2NextButton = element.shadowRoot.querySelector(".button-next");
section2NextButton.click();
const section3NextButton = element.shadowRoot.querySelector(".button-next");
section3NextButton.click();
return flushPromises().then(() => {
const recordEditFormElement = element.shadowRoot.querySelector(
"lightning-record-edit-form"
);
expect(recordEditFormElement).not.toBeNull();
const inputFieldElement = element.shadowRoot.querySelector(
"lightning-input-field"
);
inputFieldElement.value = LEADID;
expect(inputFieldElement).not.toBeNull();
inputFieldElement.dispatchEvent(new CustomEvent("change"));
const section4NextButton = element.shadowRoot.querySelector(
".button-next"
);
section4NextButton.click();
expect(invokePdfCreateService.mock.calls.length).toBe(1);
expect(invokePdfCreateService.mock.calls[0][0]).toEqual({
input: INPUT_PARAMETERS,
});
});
});
});
Import declarations are importing some of the required dependencies for running our tests. Note that you can’t use the createElement in LWC code, but you can for the unit test file.
Jest.mock is an interface that reminds me of the HTTPCalloutMock interface from Salesforce unit testing. Jest.mock mocks calls and responses to external services. In the context of our test, we can’t _actually _make an imperative call to our PdfCreateService APEX class/methods so we have to mock this call and response.
It’s important to note that this could be a point of failure in our tests because what we mock up could be different than how the actual PdfCreateService method behaves. Therefore, it’s a good idea to have a unit test on the Salesforce side that verifies that the PdfCreateService class is behaving correctly. This is actually exactly what we will do in the next session.
A DOM reset is needed after each unit test because the same jsdom instance is shared across all unit tests in the file. The code snippet below ensures that we’re working with a clean slate after each unit test so that we’re not getting erroneous test results due to leftovers in the jsdom after the previous unit test.
describe("c-car-configurator", () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
The flushPromises code snippet below helps us handle callbacks and making sure that each callback resolves properly with a Promise.
function flushPromises() {
// eslint-disable-next-line no-undef
return new Promise((resolve) => setImmediate(resolve));
}
The unit tests themselves now make up the remainder of the test file. There is a lot of material to unpack in each specific unit test in terms of how to set- p the test and invoke what needs to be tested, so we’ll have some homework to do at the end of this article. However, each individual unit test will have a general form that follows something like the code below:
it("A friendly label for your unit test", () => {
//Initialize your test case here
const element = createElement("c-component-name", {
is: ComponentName,
});
document.body.appendChild(element);
//Then perform the actions that your test is seeking to test/simulate
const button = element.shadowRoot.querySelector("button-being-tested");
button.click();
//Resolve Promises
return Promise.resolve().then(() => {
//Set a variable for the element you're inspecting
const ElementToBeTested = element.shadowRoot.querySelectorAll("selectorForElementToBeTested");
//Make assertions with the expect() function and check if you're getting the correct desired result
Const ExpectedValue = "Expected value";
expect(ElementToBeTested.property).toBe(ExpectedValue);
});
});
As you can see, even though the specific code in each unit test varies, all tests follow the same pattern from our original AAA Framework. With this framework, pretty much any element in our app, including rendering behavior, navigation, button clicks, and service callouts can be unit tested to ensure that the code is working correctly!
Concluding Thoughts
I’m going, to be honest. From someone who came from a background of unit testing for Salesforce APEX code, the topics covered here were pretty foreign to me. I think the hardest part to grasp was the Jest methods used for testing UI behavior and rendering items. For example, “shadow DOM”, “root and child elements”, and other front-end heavy items were not things I typically worried about as a Salesforce administrator or developer.
However, I also recall a time when APEX unit testing was also a pretty foreign concept to me. But once I understood the purpose of unit testing and the syntax and methods used to set up and run the tests and create assert statements to verify that the code is running properly, things became much easier. I expect the same outcome with Javascript unit testing and learning how to use tools like Jest.
For more information and advanced topics related to Javascript and LWC unit testing, check out the links to the resources below:
- Testing Lightning Web Components Trailhead Module
- Jest Objects
- Jest Mock Functions
- Jest Matchers
- Jest Configuration
- sfdc-lwc-jest library
- Test Lightning Web Components
- LWC Recipes
- LWC Recipes OSS
In the next article, we will explore more unit testing but specifically for Salesforce and APEX code.
If you haven’t already joined the official Chatter group for this series, I certainly recommend you do so. You will get the full value of the experience, ask questions, and start discussions with the group. Oftentimes, there are valuable discussions and additional references available, such as presentation slides and links to other resources and references.
About me: I’m an 11x certified Salesforce professional who’s been running my own Salesforce consultancy for several years. If you’re curious about my backstory on accidentally turning into a developer and even competing on stage on a quiz show at one of the Salesforce conventions, you can read this article I wrote for the Salesforce blog a few years ago.
Posted on February 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.