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:

  1. 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.
  2. 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.
  3. 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 file myTestSuite.js with the following content:

        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 after js // 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 after js // 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 after js // 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 after js // 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

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 after js // 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

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 after js // 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

References

results matching ""

    No results matching ""