Rearchitecting your Salesforce Lightning JavaScript for testability

A pattern to enable true unit testing in Salesforce Lightning

Kevin Vizcarra
Nintex Developers
5 min readJul 9, 2018

--

Photo by Clément H on Unsplash

I recently tried out Salesforce’s Lightning Testing Service (LTS) to write unit tests for our Lightning Components. I found that the example tests Salesforce provides with LTS act like integration tests, testing the functionality of a component and any nested components as a whole, including interactions with the Aura framework. Having recently overhauled our Apex code for true unit testability, we were looking to unit test our JavaScript logic, possibly leveraging LTS to provide a way to mock out dependencies and the Aura framework. LTS doesn’t do that, but I designed and implemented a solution that does , enabling us to truly unit test helper methods.

Being able to call a helper method from a test doesn’t automatically make it a unit test. You need to have units of code that aren’t tightly coupled to each other or the framework, and whose dependencies are either passed in or can be mocked. I’ve designed an architectural pattern for our Lightning JavaScript code that achieves this, and in this post I will step through refactoring an existing use case to implement this pattern.

Use case

If the user clicks a delete button in our component, we show a modal asking them to confirm the deletion. On confirmation, deleteItem() in the controller is called. We want to test the behavior within deleteItem().

As you can see, this method is quite verbose, but can be summarized as follows: we show a spinner, then make a request to Apex to delete the item; on success, we navigate to another page; and on failure, we hide the prompt, hide the spinner, and show an error toast.

Refactoring

Step 1: Rename controller methods as events

With this new pattern, each controller method represents an event either triggered by the framework (onInit) or user interaction (onDeleteConfirmed). Think of the controller as the entry point to your JS logic. It’s better to name your controller methods irrespective of the UI elements they’re associated with, i.e. choose onSaveRequested rather than onSaveClick. This way, you can update your UI implementation or completely move it to another component without having to touch your JS logic.

Step 2: Migrate to Helper

This one’s fairly straightforward: simply move all the logic from your controller method to a helper method. If your helper method is doing multiple things, name it after your controller method but with a handle prefix instead of on. If your helper method is doing a single unit of logic, i.e. calling out to Apex to save an item, then name it after the action done, starting with a verb, e.g. saveItem.

Step 3: Promise-ize

The method we’re intending to test, handleDeleteConfirmed, is now much more readable because of the strong composability of Promises. Returning a Promise from the helper method is a requirement for testing. Since we’re dealing with asynchronous work, the Promise’s resolve or reject state tells the testing framework when to begin assertions. The added benefit of this abstraction is that the implementation may change, e.g. Apex callout vs. Lightning Data Service, but the logic in handleDeleteConfirmed doesn’t have to.

Step 4: Abstract away implementation details

Web development is constantly evolving, and so will Lightning. In the past, you may have used a custom component to show a modal, but now you can use the built-in lightning:overlayLibrary instead. With the implementations of your UI components abstracted from your business logic, you can swap out those implementations and leave any existing logic untouched.

Step 5: Inject values rather than getting them

The attributes of a component represent the state of that component. Working with a component while its state may or may not change from one line to the next is ripe for bugs. Consider the following:

someMethod: function(component) {
component.get('v.item'); // {sortOrder: 2, ...}
this.doStuff(component);
this.fireEvent(component);
this.evaluateOtherItems(component);
component.get('v.item'); // null
this.evaluateItem(component);
},
evaluateItem: function(component) {
const item = component.get('v.item');
if (item.sortOrder !== 0) { // ERROR!
// ... other logic
}
}

evaluateItem() doesn’t care about what’s done in someMethod(), its only concern is to apply logic to an item. It’s also not obvious which method called in someMethod() mutates v.item.

Injecting the results of component.get() into the method is like working with a snapshot of the component state. You’ll reduce ambiguity, reduce the potential for bugs, and be left with very testable code.

I also recommend injecting the individual parameters from event.getParams() into your method for the same reasons. Plus, it loosens the coupling between your business logic and the fact that it was triggered from an event.

You’ll reduce ambiguity, reduce the potential for bugs, and be left with very testable code.

Enabling Unit Testing

Add this HelperProvider component to your aura folder and extend it from the component from which you’d like to test:

<c:MyComponent … extends=”c:HelperProvider”>

The check for $T, which is defined by LTS, enforces that the helper will only be provided while tests are running.

You can also use isTest() to prevent any onInit code from running during component creation in a test:

({
onInit: function(component, event, helper) {
if (!helper.isTest()) {
helper.handleOnInit(…);
}
}
//…
})

Unit Testing

Jasmine is a behavior-driven testing framework, which lets us write unit tests that closely match our use case from the beginning of this post.

…we show a spinner, then make a request to Apex to delete the item; on success, we navigate to another page; and on failure, we hide the prompt, hide the spinner, and show an error toast.

Here is the final piece of this implementation — the unit tests.

Let’s break it down:

we show a spinner,

Tested by it(‘shows a spinner’, …) by verifying showSpinner() was called.

then make a request to Apex to delete the item;

Tested by it(‘attempts to delete the item’, …) by verifying deleteItem() iwas called.

on success,

Encapsulated by describe(‘on deletion success’, …). Since we mock deleteItem() to return a resolved Promise in beforeAll(), there’s no need to setup any other mocks.

we navigate to another page;

Tested by it(‘redirects to another page’, …) by verifying navigateToAnotherPage() was called.

and on failure,

Encapsulated by describe(‘on deletion failure’, …). The mocking done in beforeEach() guarantees deleteItem() returns a rejecting Promise.

we hide the prompt,

Tested by it(‘hides deletion prompt’, …) by verifying hideDeletePrompt() was called.

hide the spinner,

Tested by it(‘hides spinner’, …) by verifying hideSpinner() was called.

and show an error toast.

Tested by it(‘shows an error toast’, …) by verifying showErrorToast() was called.

Conclusion (TL;DR)

  • Lightning Testing Service facilitates integration tests.
  • Unit testable code is code that isn’t tightly coupled to other implementations or the framework, and whose dependencies are either passed in or can be mocked.
  • The pattern described in this post is a good guideline to write testable code.
  • HelperProvider provides access and ability to unit test a component’s helper methods.

Please comment your thoughts, feedback, or any suggestions for improving this pattern.

--

--