Lightning Web Components - Testing
Happy New Year!
During the last post we explored some of the basics of developing with LWC. Now let’s see what options we have for testing.
I have to admit that I have never felt comfortable with front-end JavaScript testing on Salesforce. A few years ago I did a Dreamforce presentation on using Jasmine to test your Visualforce pages. The whole point was to lay the foundation of what was technically possible, but in practice it required so much setup and DOM mocking that it just never really caught on for my own day to day at work.
Once I started working with Aura, testing felt even more out of reach. How do I test each JavaScipt controller method? How would I mock out server calls? Did I have to make make a lightning application just for the purpose of running tests on components? With all these questions and doubts, I instead chose to ignore front-end testing all together.
To be clear, I am a huge proponent of test driven development and I generally dislike making any changes to code without tests to help me confirm that nothing else broke along the way. But when you are crunched for time, these things just sort of get push aside for later. Surely someone smarter than me will figure this out, right? This is one of the rare occassions where my procrastination actually paid off.
Enter lwc-jest
Jest is a JavaScript testing framework created by Facebook. lwc-jest is Salesforce’s extension of this framework to support testing Lightning Web Components. I have to commend the LWC team for continuing down this path of using existing, proven technologies, as well for making lwc-jest
open source. This again has made it easy for me to find answers to my questions while troubleshooting my code.
On top of that, this testing framework works from the command line and runs entirely on your local machine. That’s right, Salesforce development all from the comfort of your computer. No WiFi? No problem! You can’t view your component in your browser locally (at least, not that I know of), but you can code and run your tests without even authenticating with a scratch org or sandbox.
For those of you playing along at home, here are some resources to help you follow along:
Lesson 1: Machine setup is tricky on Windows
Salesforce’s documentation does a fantastic job on setting up Jest on your machine. That is, if you’re using a Mac, where everything worked fairly seemlessly. I tried this on a Windows machine and ran into some issues along the way. To be fair, I am not very experienced with Node.js so a lot of these problems could just be a result of my inexperience and just issues with running Node on a Windows machine.
I got to the step where I was running npm install
in Command Prompt and receive this error:
gyp ERR! configure error
gyp ERR! stack Error: Can't find Python executable "python", you can set the PYTHON env variable
Ok, so clearly I need to install python. I installed python3 and set the env variable on my machine and tried again. That threw a python syntax error. I wish I saved the error, but in the end it turns out I needed to use python2 instead of python3. This wasn’t a problem on my Mac because python2 is installed by default.
I tried again and ran into another error along the lines of needing .NET Framework 2.0 SDK installed, which I could resolve by installing Visual Studio. I tried that and the error didn’t go away. This was a couple hours into this and while the issue was probably due to something I was or was not doing, I couldn’t help but wonder if the package itself just had Windows issues that were beyond me. Either way, I gave up and tried again using Windows Subsystem for Linux (WSL), which I already had set up on my machine.
Setup worked out fine once I switched to WSL, but there were still some issues. Sometimes tests would fail and then suddenly pass when run again. This didn’t seem to happen on my Mac. Also, the tests ran about 4 times faster on my Mac, though that could be because I’m running the tests in WSL instead of native Linux. The difference was about 5 seconds on a Mac compared to 20 seconds on Windows. This is still blazingly fast compared to some Apex tests, but it was still consistently noticeable.
I hit a few bumps along the way, but I’m happy that Jest was working and I could get to fun part of writing tests.
Lesson 2: What am I testing again?
Before I jumped in, I had to wrap my head around front-end testing. Apex testing comes fairly naturally to me now: set up your expected data, call the methods with your inputs, does this method do what I expect it to? Does this flow do what I expect it to? I don’t know why this never translated well for me when it came to front-end testing. An application could do so much, what was necessary for testing? Do I need to assert on pixels in the margins? That tags had the appropriate class names? This analysis paralysis usually led to inaction with me just settling with “I’ve clicked through the front-end, I’m sure it’s fine”.
Since LWC is component based, this was a little easier to digest. Components tend to do fairly discrete things and if they don’t maybe the component is doing too much. lwc-jest
made isolating your tests even easier since it allowed you to render a component by itself, call its methods, and interact with its public properties.
With this modularization in mind, I came to the conclusion that I should test what I can access in the test, coming up with the following essentials:
- Does the component display the tags and data you want to display?
- Does the component respond to interactions as expected?
- Does the component’s public methods and properties (i.e. those with the
@api
decorator) work as expected?
You can always test more later, but I consider these the MVP of your tests. In the tests for the inputTableCell
component, I focus just on that:
- Are the passed in
@api
decorated properties forname
andrecord
used to display the correct data properly? - Is the
lightning-input
element present? - Does the
@api
decorated methodinputValue()
return the correct data after you input data?
beforeEach(() => {
element = createElement('c-input-table-cell', { is: InputTableCell });
element.record = { 'Name' : 'Test Name'};
element.field = 'Name';
document.body.appendChild(element);
lwcInput = element.shadowRoot.querySelector('lightning-input');
});
describe('Input table cell', () => {
it('should have the correct label and name', () => {
expect(lwcInput.label).toBe('Name');
expect(lwcInput.name).toBe('Name');
});
it('displays an input field', () => {
expect(lwcInput.value).toBe('Test Name');
});
it('returns inputted values', () => {
const expectedName = 'Updated Name'
lwcInput.value = expectedName;
const typeEvent = new Event('change');
lwcInput.dispatchEvent(typeEvent);
const inputValue = element.inputValue();
expect(inputValue.value).toBe(expectedName);
expect(inputValue.field).toBe('Name');
})
});
I really like this testing format. The describe methods create easy to read documentation as to what this component does without having to hijack method names like I do in Apex (e.g. @isTest private static void shouldReturnInputValueWhenChanged()
)
Lesson 3: Get good at query selectors and events
In Jest, you are working with a simulated DOM as if you were in the browser. In other words, the tests don’t actually render the page, at least, not in a way that you can look at and interact with it. You will need to write JavaScript query selectors in order to assert on whether or not they present. You’ll also have to fire off your own Events when you interact with your component. For example, let’s take the input from inputTableCell
:
<lightning-input variant="label-hidden" label={field} name={field} value={value} onchange={handleInputChange}></lightning-input>
When the input is changed, handleInputChange()
should be called, which in turn updates the value returned by the public method inputValue()
. In my test, I initially tried the following:
it('returns inputted values', () => {
const expectedName = 'Updated Name'
lwcInput.value = expectedName;
const inputValue = element.inputValue();
expect(inputValue.value).toBe(expectedName);
})
This didn’t work because updating the value on the element doesn’t actually fire the change event. You need to do that yourself like so:
const typeEvent = new Event('change');
lwcInput.dispatchEvent(typeEvent);
Again, standards come to rescue as I was easily able to find this documented online.
However, whenever my tests failed I couldn’t help but wonder if I had just written an Event or a query selector incorrectly. At first I was just running my tests over and over again, but once I figured out how to use the interactive debugger, I was able to save myself some time.
Lesson 4: Use the debugger to interact with your code during execution
If you run your tests using the debug flag (i.e. lwc-jest --debug
), tests will actually stop at breakpoints in your code so you can interact with the code in that context. For example, I placed a random debugger in my tests:
it('should have the correct label and name', () => {
console.log('here');
debugger;
expect(lwcInput.label).toBe('Name');
expect(lwcInput.name).toBe('Name');
});
To interact with the debugger, in Chrome go to chrome://inspect
and click “Open dedicated DevTools for Node”
When you run your tests the debugger will freeze once, indicating tests are ready to run. Press the play button and the debugger will eventually freeze again at your breakpoint, where you can interact with the variables in that context. (This may take some time based on how fast your tests run)
You’ll notice that nothing is rendered in the browser, you just have the console to work with. This was particularly useful for testing query selectors to see if you are writing them correctly, as well as trying to figure out what is rendered on the page in general without being able to look at it. This became especially useful when doing integration tests with components that were composed of other components.
Lesson 5: Components are only rendered one level deep
The highest level component in my project was demoComposition
, which contains two multiEditTable
components, which each contain inputTableCell
. During my test of demoComposition
, I was trying to interact with the inputs rendered by inputTableCell
, but the inputs just weren’t there.
I might be missing something, but this Stack Overflow post seems to indicate there this is some kind of “shallow” rendering mode and there is some way to render all of the components. I didn’t figure it out, so I left it at just testing that the top level components are rendered. This was a shame since I couldn’t try out testing the interaction with the Apex controller. This is something I would like to dig into more to see what options I have in terms of rendering more of the hierarchy, or even just mocking out the child components.
Conclusions
There is still a lot more to explore with LWC. For example, I’d like to do more with the @wire
decorator and mixing aura components with LWC. But while there a still some kinks to work out, I am very excited to start adding LWC to production applications. Not only will this open up Salesforce platform development to more traditional JavaScript developers, but it also makes front-end development much more of a joy for those us already in the system. The tools provided with LWC will help automate some of the simpler front-end QA that we usually manually perform, help developers make front-end changes with more confidence that they aren’t causing unexpected bugs, and lead to generally cleaner and more modular code. I encourage every Salesforce developer to give Lightning Web Components a try and I would love to see what the community is making so feel free to shoot me an email or a tweet to share some code.
Lightning Web Components - First Impressions
I am not a huge of fan of Aura, at least conceptually. As a tool it is fine, but a JavaScript framework seemed like a good opportunity to bridge the gap between Salesforce and non-Salesforce web development. Instead, we got Aura which may as well have been another Salesforce specific language that you would have to learn in order the break into the already niche world of Salesforce development. So when they announced Lightning Web Components (LWC) earlier this month I was skeptical. JavaScript fatigue is bad enough - Salesforce specific JavaScript fatigue seems almost comical.
However, the blog post seemed to indicate that they feel the same way:
Frameworks became the language. React, Angular, and the Lightning Component Framework are all JavaScript frameworks, but they provide such a high level of abstraction that they feel like different languages. As a result, skills were not transferable, and developers were hard to find and ramp up.
And their solution:
Lightning Web Components is the Salesforce implementation of that new breed of lightweight frameworks built on web standards. It leverages custom elements, templates, shadow DOM, decorators, modules, and other new language constructs available in ECMAScript 7 and beyond.
A standards based approach was intriguing to me, so I wanted to give LWC a spin and share what I learned along the way.
Dynamic Multi Line Editable Table
Something I have built over and over again is a mechanism to input multiple lines of input and then submit it to the server for whatever processing. I wanted to be able to feed a component an array of columns and it dynamically builds my table for me. I wanted to able to code something like this:
<c-multi-edit-table
column-list='\[{ "label" : "Name", "apiName" : "Name"} , { "label" : "Email", "apiName" : "Email" }, { "label" : "Phone", "apiName" : "Phone" }]'
title='Attendees'>
</c-multi-edit-table>
<c-multi-edit-table
column-list='\[{ "label" : "Fee Name", "apiName" : "FeeName" }, { "label" : "Amount", "apiName" : "Amount" }]'
title='Estimated Fees'>
</c-multi-edit-table>
And get this:
For those of you playing along at home, here are some resources to help you get started:
Quick Start: Lightning Web Components
Lesson #1: Naming Matters
I actually learned this last, but I thought it was very important to point out and save someone an hour trying to figure out what’s wrong.
A component is composed of three parts: HTML, JS, and CSS
multiEditTable
|_ multiEditTable.html
|_ multiEditTable.js
|_ multiEditTable.css
Which you can then reference in other components by using
<c-multi-edit-table></c-multi-edit-table>
Turns out that the naming is very important here. The camel casing is parsed and each word is separated with a -
. This causes an issue if you start your component with a capital letter like I originally did (i.e. MultiEditTable). In order to reference it you’ll need to use:
<c--multi-edit-table></c--multi-edit-table> <!-- notice the two dashes before "multi" -->
The hard part about working with a pre-release is not knowing what is a bug and what is working as intended. My guess is that the parser is putting a dash before every capital letter, but the autocomplete in the VS Code plugin didn’t pick this up so I spent an hour or so trying to figure out why my component would not render. The only error message I would get is Compilation Failed
. I doubt this is the way it is intended to work, so to be safe just use a lower case letter when naming your components.
Lesson #2: Computed Keys Still Not Allowed
One obstacle I always ran into while doing something dynamic like this was working with computed keys within the component markup. Here’s something you can’t do in Aura:
<aura:attribute name="record" type="Object[]"/>
<aura:iteration items="{!v.columns}" var="column">
<lightning:input label="{!v.column}" value="{!v.record[column]}"/>
</aura:iteration>
Aura won’t let you bind computed key values (i.e. value="{!v.record[column]}"
is invalid). To get around this, I had to create 3 separate components - 1 for the table itself, 1 for each row, and one for each input column within each row. It creates so much code and the components are so tightly coupled that you would never use them separtely. Here’s a basic version of the end product in aura:
-- Table component
<aura:component>
<aura:attribute name="record" type="Object[]"/>
<aura:attribute name="columns" type="String[]"/>
<aura:iteration items="{!v.records}" var="record">
<c:InputRow row="{!v.record}" columsn="{!v.columns}"/>
</aura:iteration>
</aura:component>
-- InputRow Component
<aura:component>
<aura:attribute name="row" type="Object"/>
<aura:attribute name="columns" type="String[]"/>
<aura:iteration items="{!v.columns}" var="column">
<c:InputCell record="{!v.row}" field="{!v.column}"/>
</aura:iteration>
</aura:component>
-- InputCell Component
<aura:component>
<aura:attribute name="record" type="Object"/>
<aura:attribute name="field" type="String"/>
<aura:attribute name="value" type="String"/>
<lightning:input value="{!v.value}" onchange="{!c.updateRecord}"> --updateRecord would set the field value on record
</aura:component>
If you could use computed keys, this would actually be fairly straightfoward. Here’s what I tried first using LWC:
<template for:each={record} for:item="record">
<template for:each={columns} for:item="column">
<td key={column}>
<lightning-input name={column} value={record[column]}></lightning-input>
</td>
</template>
</template>
Unfortunately this still does not work and the compiler explicitly states that you cannot use computed keys. I was so disappointed that I almost stopped there, but I figured I had gotten this far and I might as well see if LWC would provide other benefits.
Lesson #3: One-Way vs. Two-Way Binding
When I built this table using Aura, I created an array of Objects in the top most component and passed the individual Objects in that array down the hierarchy.
<aura:attribute name="records" type="Object[]"/>
<aura:iteration items="{!v.records}" var="record">
<c:InputRow row="record">
</aura:iteration>
In Aura, you can define an attribute as having one way or two binding using {#v.row}
and {!v.row}
respectively. Two way binding made keep tracking of changes in the rows easy. You can pass an Object all the way down the hierarchy of components and any changes to that Object are automatically propagated back to the parent for you.
With LWC, you only get one-way binding; there’s no option for two-way binding. At first I was frustrated by this because if I wanted to mimic the two-way binding, I thought I would have to deal with broadcasting and handling events. However, the more I thought about it, the more I realized that enforcing one-way binding would lead to less side effects. Components now have to explicitly change their properties instead of having them get changed by another component due to a shared reference to an object where it is likely tracked using a different variable name. In the previous example the component tracked the objects using the variable record
, whereas the InputRow
component used row
. When working with more complex components keeping track of these variable pairings in your head could make debugging tricky.
Lesson #4: Standard Query Selectors
That aside, I still needed to track the changes made between each row. At this point, I didn’t see any of the productivity increases that I was hoping for - I still needed to create multiple levels of components and now I needed build an event so I could capture the row changes. Then I realized that I was still thinking with an Aura mindset.
I started to wonder how I would solve this if I was just building this with vanilla JavaScript. I would probably just use document.querySelectorAll('tr')
and just iterate over each input within each row and build an array of objects from there. In LWC, you can do just that. For example, here’s how you can get the inputs from each row:
retrieveRecords() {
var records = [];
Array.from( this.template.querySelectorAll("tr.inputRows") ).forEach(row => {
var record = {};
Array.from( row.querySelectorAll("input") ).forEach(input => {
record[input.name] = input.value;
});
records.push(record);
})
return records;
}
This is where LWC started to click for me. With LWC, I could think with a more standard mindset instead of trying to solve Salesforce specific problems with Salesforce specific tools. More importantly, I could search for my JavaScript questions on Google without having to prepend every query with salesforce
.
Lesson #5: More Intuitive Server Calls
Now that I have all the data I need, I needed to submit it to my Apex controller to do whatever processing (i.e. a database insert). This was much clearer in LWC.
In Aura:
submit : function(component, event, helper) {
var action = component.get("c.submitRecords");
action.setParams({
"records": JSON.stringify(component.find("inputTable").get("v.recordList")),
});
action.setCallback(this, function(response) {
var state = response.getState();
if (state === "SUCCESS") {
alert('Success!');
} else if (state === "ERROR") {
alert('Something went wrong...');
}
});
$A.enqueueAction(action);
}
And the same method in LWC:
import { LightningElement } from 'lwc';
import submitRecords from '@salesforce/apex/MultiEditTableController.submitRecords';
export default class DemoComposition extends LightningElement {
submit() {
var allRecords = this.template.querySelector("c-multi-edit-table").retrieveRecords();
submitRecords({ records: allRecords })
.then(() => {
alert('Success!');
})
.catch(() => {
alert('Something went wrong...');
})
}
}
I don’t want to argue style or readability, but what I really like about LWC is that it is very clear where everything is coming from. The submitRecords
controller method is explicitly referenced in LWC so you know which Apex class it’s referencing. Also because of the way LWC uses import
to bring in Apex methods, you could in theory include methods from other Apex classes fairly easily. In Aura, you’d have to create a separate service component that hooked up to another Apex controller and then include that commponent for the sole purpose of accessing those methods.
Next Steps:
After a few hours I was successfully able to create my table. This came as a surprise because we’re still in the pre-release stage and there’s not much documentation out there. LWC’s adherence to web standards made this less of an issue because a lot of the problems I ran into weren’t all Salesforce specific problems. I could not say the same about my initial experience with Aura.
However, the biggest surprise was something I haven’t touched upon yet. In the next post, we’ll cover testing LWC using Jest, all from the comfort of your local machine.
Configuration Driven Development
When introducing people to Salesforce development, early on I usually hear something along the lines of:
I don’t understand what’s so special about Salesforce. It just seems like an expensive database with some minor drag and drop logic
I struggled with this idea for a long time. The slogan Clicks not code always irked me. At best it downplays the value of an engineer. At worst it was the cause of common anti-patterns as people would try to piece together a convoluted mixture of formulas and workflows when writing the same in Apex would be clearer and have the added benefit of being tested. In fact, when Process Builder was released, I took it as a slight towards developers; they were actively working towards making developers obsolete as opposed to improving the tooling. And don’t get me started on Sassy.
No software, EXCEPT FOR EVERYTHING
I treated the configurable portions of Salesforce as a separate app that also happened to touch the same database. The way I wrote Apex would be as if I wrote Ruby without leveraging the benefits of Rails. Sure, I would use configurable components like custom metadata, field sets, and custom labels to make things a little more dynamic. And I never griped about not having to worry as much about security breaches and server maintenance. But I always felt like I was developing on top of Salesforce instead of extending it.
This changed about a year ago when I was trying to embed a report into a Lightning application. Out of the box, you can embed a report chart, but not the report itself and my users basically wanted a summary list view. My first instinct was to just build a component that displayed the results of a relatinoship SOQL query. I even thought I should store the SOQL queries as strings so you could quickly spin up new components.
But my mind kept going back to the reports. If I could just display the results of a report, a majority of the work would be complete. I wouldn’t need to worry about how to model the data, just how to display the results. The result was a Lightning component that displayed the results of a summary report driven by the JSON output of the Reporting and Dashboards API.
This was topic of my talk at Dreamforce ‘17, of which you can view the slides and audio recording in this GitHub repo.
But after building this component, the real benefits of Salesforce to an engineer became clear to me. Aside from any technical bugs that appeared, I did not have to maintain this component. An admin could manage changes using the standard report builder and easily add new report components without any help from a developer. Instead of putting in hours maintaining slight changes, I could focus on enhancing functionality.