Test Automation - Lightning Testing Service (LTS)
The Lightning Testing Service, or LTS, is a set of tools and services that let you create test suites for your Lightning components using standard JavaScript test frameworks, such as Jasmine and Mocha.
Automated tests are the best way to achieve predictable, repeatable assessments of the quality of your custom code.
Writing automated tests for your custom components gives you confidence that they work as designed, and allows you to evaluate the impact of changes, such as refactoring, or of new versions of Salesforce or third-party JavaScript libraries.
NOTE:
- Don’t run tests in your production org.
- The LTS doesn’t provide an isolated test context or transaction wrapper.
- DML operations you perform in your tests won’t be rolled back at the end of the test.
- We recommend that you run your LTS test suites only in scratch orgs, using data provided by the test suite itself.
Getting started with LTS
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 (change in the dependency tree) or when their dependencies (the framework, another component, or a JavaScript library they depend on) are updated.
The Lightning Testing Service consists of:
- Wrappers that allow you to use popular JavaScript testing frameworks within your Salesforce org. LTS currently provides wrappers for Jasmine and Mocha, and you can create your own wrappers for other frameworks.
- A utility object ($T) that makes it easy to work with Lightning Components in your tests. For example, it allows you to instantiate components or fire application events.
- A tight integration with Salesforce DX that allows you to run tests from the command line and from within continuous integration scripts.
Note:
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.
Jasmine concepts
- A test suite is a plain JavaScript file wherein related tests (specs) are organized within
describe()
functions. - A spec (or test) consists of one or more expectations invoked within the it() function.
- An expectation is an assertion that evaluates to either true or false.
- A spec is passing when all its expectations evaluate to true. A spec is failing when at least one of its expectations evaluates to false.
Installing LTS - SFDX
Installing the LTS is a one line command in the Salesforce DX CLI. The LTS provides utilities specific to the Lightning Component framework, which let you test behavior specific to Lightning components.
$ sfdx update
$ sfdx --version
sfdx-cli/6.15.0-ded9afdffb (darwin-x64) node-v8.9.4
$ sfdx force:lightning:test:install
Installing LTS - without SFDX
If you are not using Salesforce DX, you can install the LTS package manually. Go to the LTS project release page and click on the link for the latest version of LTS with Examples.
/**
* Jasmine: This is a 'hello world' Jasmine test spec
*/
describe("A simple passing test", function() {
it("checks that true is always true", function() {
expect(true).toBe(true);
});
});
Run Sample test suite via SFDX
$ sfdx force:lightning:test:run -a jasmineTests.app
Run Sample test suite via Browser
open https://<BASE_URL>/c/jasmineTests.app
Creating and running your own test suite
- Create a test suite
- in
force-app/main/default/staticresources
create a js filemyTestSuite.js
with the following content:
- in
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);
});
});
// add your test code here
})
sfdx 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.
- In the folder:
force-app/main/default/staticresources
create xml file:myTestSuite.resource-meta.xml
with the content:
<?xml version="1.0" encoding="UTF-8"?>
<StaticResource xmlns="http://soap.sforce.com/2006/04/metadata">
<cacheControl>Private</cacheControl>
<contentType>application/javascript</contentType>
</StaticResource>
- Create a test Lightning Test Application
MyTestApp
:
$ sfdx force:lightning:app:create -n MyTestApp -d force/main/default/aura/
# in VS Code: View > Command Palette > SFDX: Create Lightning App
MyTestApp.app markup:
<aura:application>
<c:lts_jasmineRunner testFiles="{!$Resource.myTestSuite}" />
</aura:application>
- Push MyTestApp into your org:
$ sfdx force:source:push
- Run tests by running MyTestApp.app
$ sfdx force:lightning:test:run -a myTestApp.app
# via browser: https://<BASE_URL>/c/myTestApp.app
Examples
1. Verify component rendering
Write a test to verify that a component renders as expected.
- Component markup
<!-- helloWorld component -->
<aura:component>
<div aura:id="message">Hello World!</div>
</aura:component>
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe('c:helloWorld', function () {
it('verify component rendering', function (done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
// {} optional list of attribute values
// true: test requires the component to be rendered?
$T.createComponent('c:helloWorld', {}, true)
.then(function(cmp) {
// check that weget innerHTML contains 'Hello World!'
// for this component's element with id as 'message'
expect(cmp.find("message").getElement().innerHTML).toBe('Hello World!');
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app
2. Verify Data Binding
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.
- Component markup
<!-- componentWithDataBinding component -->
<aura:component>
<aura:attribute name="message" type="String"/>
<!-- get user input -->
<lightning:input aura:id="messageInput" value="{!v.message}"/>
<!-- div UI element bound this attribute -->
<div aura:id="message">{!v.message}</div>
</aura:component>
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe('c:componentWithDataBinding', function () {
it('verify data binding', function (done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
// {} optional list of attribute values: {message: 'Farming wins!'}
// true: test requires the component to be rendered?
$T.createComponent('c:componentWithDataBinding', {message: 'Farming wins!'}, true)
.then(function (component) {
// verify div element with id:message has this attribute correctly?
expect(component.find("message").getElement().innerHTML).toBe('Farming wins!');
// verify lightning:input has this attribute correctly?
expect(component.find("messageInput").get("v.value")).toBe('Farming wins!');
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app
3. Verify method invocation
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.
- Component markup
<!-- componentWithMethod component -->
<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);
}
})
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe("c:componentWithMethod", function() {
it('verify method invocation', function(done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
// {} optional list of attribute values
// false: test requires the component to be rendered?
$T.createComponent("c:componentWithMethod", {}, false)
.then(function (component) {
expect(component.get("v.counter")).toBe(0);
// execute the 'increment' method on the component
component.increment();
// did we get the incremented value?
expect(component.get("v.counter")).toBe(1);
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app
4. Verify application event
Verify that a component listening to an application event works as expected when the application event is fired.
- Component and event markups
<!-- componentListeningToAppEvent component -->
<aura:component>
<aura:attribute name="message" type="String" />
<aura:handler event="c:myAppEvent" action="{!c.handleAppEvent}" />
</aura:component>
<!-- myAppEvent Application Event -->
<aura:event type="APPLICATION">
<aura:attribute name="msg" type="String" />
</aura:event>
componentListeningToAppEventController.js
({
handleAppEvent : function(component, event, helper) {
component.set("v.message", event.getParam("msg"));
}
})
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe('c:componentListeningToAppEvent', function () {
it('verify application event', function (done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
$T.createComponent("c:componentListeningToAppEvent")
.then(function (component) {
// fire the event: myAppEvent with { "msg": "event fired"}
$T.fireApplicationEvent("c:myAppEvent", {"msg": "event fired"});
// check handleAppEvent has set the attribute: message properly
expect(component.get("v.message")).toBe("event fired");
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app
5. Verify server method invocation (not recommended)
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 example 6 below for an example demonstrating how to mock the server method invocation all together.
// AccountController.cls - server-side controller
global with sharing class AccountController {
// make it available to Lightning
@AuraEnabled
public static Account[] getAccounts() {
// mock account records
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;
}
}
- Component markup
<!-- accountList component using the AccountController server-side controller -->
<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) {
// setting action for calling server-side controller's getAccounts() method
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);
}
})
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe('c:accountList', function () {
it('verify server method invocation', function (done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
$T.createComponent("c:accountList")
.then(function (component) {
// before executing the server-side call the lenght of the array accounts should be 0
expect(component.get("v.accounts").length).toBe(0);
// run the method loadAccounts
$T.run(component.loadAccounts);
/*
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.
*/
return $T.waitFor(function () {
// make sure that we have 3 records in the response
return component.get("v.accounts").length === 3;
})
}).then(function () {
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app
6. Verify mocked server method invocation (recommended)
Instead of calling a remote method that returns mock data (as shown in #5), 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.
// AccountController.cls - server-side controller
global with sharing class AccountController {
// make it available to Lightning
@AuraEnabled
public static Account[] getAccounts() {
// mock account records
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;
}
}
- Component markup
<!-- accountList component using the AccountController server-side controller -->
<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) {
// setting action for calling server-side controller's getAccounts() method
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);
}
})
- Add the following js code into
myTestSuite.js
afterjs // add your test code here
// c: is default namespace, replace it with your namespace
describe('c:accountList', function () {
it('verify mocked server method invocation', function (done) {
// $T.createComponent(componentName) : instantiates a Lightning component.
$T.createComponent("c:accountList", {}, true)
.then(function (component) {
// mocking the response
var mockResponse = {
// response state as SUCCESS
getState: function () {
return "SUCCESS";
},
getReturnValue: function () {
// mock 3 account records
return [{"Name": "Farming"}, {"Name": "Weaving"}, {"Name": "Carpentry"}];
}
};
// intercept (hijack) on $A.enqueueAction() and callFack(...)
spyOn($A, "enqueueAction").and.callFake(function (action) {
var cb = action.getCallback("SUCCESS");
// apply our mockResponse to become the response
cb.fn.apply(cb.s, [mockResponse]);
});
// run the method loadAccounts
component.loadAccounts();
// do we have 3 account records
expect(component.get("v.accounts").length).toBe(3);
// first account record name is 'Farming'?
expect(component.get("v.accounts")[0]['Name']).toContain("Farming");
done();
}).catch(function (e) {
done.fail(e);
});
});
});
- Run the test suite
sfdx force:lightning:test:run -a MyTestApp.app