As the number and complexity of your Lightning Components grows, so does the risk of defects when you write them, and of breaking changes when you refactor them. Automated testing allows you to mitigate those risks and verify that your components work as designed when you first write them, and continue to work as expected when you make changes or when their dependencies (the framework, another component, or a JavaScript library they depend on) are updated.
This post provides an overview of the Lightning Testing Service (LTS) and how it makes it easy to test your Lightning Components using popular JavaScript frameworks.
The Lightning Testing Service consists of:
Although the LTS integration with Salesforce DX is powerful, Salesforce DX is not a requirement to use LTS. You can install the LTS package manually and run your tests from a browser window.
In this article, we use Jasmine to illustrate how to run automated tests against Lightning Components. If you are new to Jasmine, here are some key concepts you need to be aware of:
Here is a canonical Jasmine test suite:
describe("A suite that tests the obvious", function() { it("spec that verifies that true is true", function() { expect(true).toBe(true); }); it("spec that verifies that false is false", function() { expect(false).toBe(false); }); });
Check out the Jasmine documentation to learn more.
Follow the steps below to install LTS and run the sample test suite available in the LTS package.
Type the following command to install the latest version of the LTS package in your default scratch org:
sfdx force:lightning:test:install
If you are new to Salesforce DX, check out the Salesforce DX Quick Start to learn how to create a scratch org.
NOTES:
LTS comes with a sample test suite for Jasmine and Mocha. Type the following command to run the sample test suite for Jasmine:
sfdx force:lightning:test:run -a jasmineTests.app
The test results appear in the console as illustrated below.
You can also run the test suite from the browser by accessing the following URL (replace <BASE_URL> with your org’s base URL): https://<BASE_URL>/c/jasmineTests.app
The test results appear in the browser window as illustrated below.
A Jasmine test suite is a plain JavaScript file that contains a series of related specs. Using LTS, you create the Jasmine test suite as a static resource.
describe("Lightning Component Testing Examples", function () { afterEach(function () { $T.clearRenderedTestComponents(); }); describe("A suite that tests the obvious", function() { it("spec that verifies that true is true", function() { expect(true).toBe(true); }); }); });
NOTE: force:source:push supports automatic source transformation when working with static resources. In this specific example, it means you can edit the file in its .js format. The file is automatically transformed to the right static resource format when pushed to your scratch org. Check out the documentation for details. You still have to create the -meta.xml file as described below.
<?xml version="1.0" encoding="UTF-8"?> <StaticResource xmlns="http://soap.sforce.com/2006/04/metadata"> <cacheControl>Private</cacheControl> <contentType>application/javascript</contentType> </StaticResource>
sfdx force:lightning:app:create -n MyTestApp -d force-app/main/default/aura/
NOTE: If you are using Visual Studio Code with the Salesforce DX extensions, you can also create an application from the command palette (View > Command Palette > SFDX: Create Lightning App)
<aura:application> <c:lts_jasmineRunner testFiles="{!$Resource.myTestSuite}" /> </aura:application>
Type the following command to push your test suite static resource and your test application to your scratch org:
sfdx force:source:push
Type the following command to run the tests:
sfdx force:lightning:test:run -a myTestApp.app
You can also run the test suite from the browser by accessing the following URL (replace <BASE_URL> with your org’s base URL): https://<BASE_URL>/c/myTestApp.app
Now that you have the infrastructure in place, you can start adding tests to your test suite. In this section, we look at some common testing scenarios.
In this first example, we write a test to verify that a component renders as expected.
<aura:component> <div aura:id="message">Hello World!</div> </aura:component>
describe('c:helloWorld', function () { it('verify component rendering', function (done) { $T.createComponent('c:helloWorld', {}, true) .then(function(cmp) { expect(cmp.find("message").getElement().innerHTML).toBe('Hello World!'); done(); }).catch(function (e) { done.fail(e); }); }); });
Code highlights:
sfdx force:lightning:test:run -a myTestApp.app
In this example, we instantiate a component passing a value for the message attribute and we verify that UI elements bound to that attribute are displaying the expected value.
<aura:component> <aura:attribute name="message" type="String"/> <lightning:input aura:id="messageInput" value="{!v.message}"/> <div aura:id="message">{!v.message}</div> </aura:component>
describe('c:componentWithDataBinding', function () { it('verify data binding', function (done) { $T.createComponent('c:componentWithDataBinding', {message: 'Hello World!'}, true) .then(function (component) { expect(component.find("message").getElement().innerHTML).toBe('Hello World!'); expect(component.find("messageInput").get("v.value")).toBe('Hello World!'); done(); }).catch(function (e) { done.fail(e); }); }); });
In this example, we verify that a method invocation produces the expected result. We create a component with a counter attribute and a method that increments that counter. We verify that the method invocation increments the counter as expected. Note that you cannot directly invoke methods in the component’s controller or helper. You can only invoke methods that are exposed as part of the component’s public API with an <aura:method> definition.
<aura:component> <aura:attribute name="counter" type="Integer" default="0"/> <aura:method name="increment" action="{!c.increment}"/> </aura:component>
componentWithMethodController.js
({ increment : function(component, event, helper) { var value = component.get("v.counter"); value = value + 1; component.set("v.counter", value); } })
describe("c:componentWithMethod", function() { it('verify method invocation', function(done) { $T.createComponent("c:componentWithMethod", {}, false) .then(function (component) { expect(component.get("v.counter")).toBe(0); component.increment(); expect(component.get("v.counter")).toBe(1); done(); }).catch(function (e) { done.fail(e); }); }); });
Code Highlight:
The third argument of $T.createComponent() is false: this test doesn’t require the component to be rendered.
In this example, we verify that a component listening to an application event works as expected when the application event is fired.
<aura:event type="APPLICATION"> <aura:attribute name="message" type="String" /> </aura:event>
<aura:component> <aura:attribute name="message" type="String" /> <aura:handler event="c:myAppEvent" action="{!c.handleAppEvent}" /> </aura:component>
componentListeningToAppEventController.js
({ handleAppEvent : function(component, event, helper) { component.set("v.message", event.getParam("message")); } })
describe('c:componentListeningToAppEvent', function () { it('verify application event', function (done) { $T.createComponent("c:componentListeningToAppEvent") .then(function (component) { $T.fireApplicationEvent("c:myAppEvent", {"message": "event fired"}); expect(component.get("v.message")).toBe("event fired"); done(); }).catch(function (e) { done.fail(e); }); }); });
In this example, we verify that a call to a method in the component’s Apex controller works as expected. Performing real database operations from within Lightning tests is not recommended because Lightning tests don’t run in an isolated testing context, and changes to data are therefore permanent. Tests that rely on real data also tend to be nondeterministic and unreliable. Instead of accessing real data, consider using mock (static and predictable) data as demonstrated in this example. Moreover, tests that involve communication over the network are generally not recommended. The recommended best practice is to test the client and server code in isolation. See scenario 6 below for an example demonstrating how to mock the server method invocation all together.
global with sharing class AccountController { @AuraEnabled public static Account[] getAccounts() { List accounts = new List(); accounts.add(new Account(Name = 'Account 1')); accounts.add(new Account(Name = 'Account 2')); accounts.add(new Account(Name = 'Account 3')); return accounts; } }
<aura:component controller="AccountController"> <aura:attribute name="accounts" type="Account[]" /> <aura:method name="loadAccounts" action="{!c.loadAccounts}" /> </aura:component>
accountListController.js
({ loadAccounts : function(component, event, helper) { var action = component.get("c.getAccounts"); action.setCallback(this, function (response) { var state = response.getState(); if (state === "SUCCESS") { component.set("v.accounts", response.getReturnValue()); } else if (state === "INCOMPLETE") { $A.log("Action INCOMPLETE"); } else if (state === "ERROR") { $A.log(response.getError()); } }); $A.enqueueAction(action); } })
describe('c:accountList', function () { it('verify server method invocation', function (done) { $T.createComponent("c:accountList") .then(function (component) { expect(component.get("v.accounts").length).toBe(0); $T.run(component.loadAccounts); return $T.waitFor(function () { return component.get("v.accounts").length === 3; }) }).then(function () { done(); }).catch(function (e) { done.fail(e); }); }); });
Code highlight:
The $T.waitFor(function, timeout, interval) function checks at a regular interval (defined by the value passed as the third argument) for a set amount of time (defined by the value passed as a second argument) if the function passed as the first argument returns true. If the function returns true within the allotted time frame, the promise is resolved and the test succeeds. If the function doesn’t return true within the allotted time frame, the promise is rejected and the test fails.
Instead of calling a remote method that returns mock data, you can mock the server call all together. Jasmine provides a spy utility that allows you to intercept (hijack) calls to specific functions. To mock a remote method invocation call, all you have to do is spy on the $A.enqueueAction() function and provide a mock implementation to execute instead of sending the request to the server. Let’s create a new test for the accountList component using this approach. The Apex controller and Lightning component don’t change.
describe('c:accountList', function () { it('verify mocked server method invocation', function (done) { $T.createComponent("c:accountList", {}, true) .then(function (component) { var mockResponse = { getState: function () { return "SUCCESS"; }, getReturnValue: function () { return [{"Name": "Account 1"}, {"Name": "Account 2"}]; } }; spyOn($A, "enqueueAction").and.callFake(function (action) { var cb = action.getCallback("SUCCESS"); cb.fn.apply(cb.s, [mockResponse]); }); component.loadAccounts(); expect(component.get("v.accounts").length).toBe(2); expect(component.get("v.accounts")[0]['Name']).toContain("Account 1"); done(); }).catch(function (e) { done.fail(e); }); }); });
Other testing scenarios are available in the LTS repository. For example, check out the jasmineLightningDataServiceTests test suite to learn how to write tests for the Lightning Data Service.
The Lightning Testing Service (LTS) makes it easy to test your Lightning Components using popular JavaScript frameworks like Jasmine and Mocha. Check it out today and see how it can help you build and maintain components with confidence by automatically verifying that they work as designed when you first write them, and continue to work as expected when you modify them.