Getting Started with the Lightning Testing Service

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:

  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.

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 101

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:

  • 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.

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.

Installation instructions

Follow the steps below to install LTS and run the sample test suite available in the LTS package.

Step 1: Install LTS

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:

  • 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.
  • Do not install LTS in a production org. Lightning tests don’t run in an isolated testing context, and DML operations you perform in a test are not rolled back when the test completes.

Step 2: Run the sample test suite

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.

Creating and running your own test suite

Step 1: Create a test suite

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.

  1. In force-app/main/default/staticresources, create a file named myTestSuite.js and paste 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);
            });
        });
    });
    

    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.

  2. In force-app/main/default/staticresources, create a file named myTestSuite.resource-meta.xml and paste the following 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>
    

Step 2: Create a test application

  1. Type the following command to create a Lightning Application named myTestApp used to run your test suite:
    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)

  2. Implement myTestApp.app as follows:
    <aura:application>
        <c:lts_jasmineRunner testFiles="{!$Resource.myTestSuite}" />
    </aura:application>
    

Step 3: Push your changes

Type the following command to push your test suite static resource and your test application to your scratch org:

sfdx force:source:push

Step 4: Run your tests

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

Common Lightning Components testing scenarios

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.

Scenario 1: Verify component rendering

In this first example, we write a test to verify that a component renders as expected.

    1. Create a component called helloWorld implemented as follows:
      <aura:component>
          <div aura:id="message">Hello World!</div>
      </aura:component>	
      
    2. Add the following test to your test suite (in myTestSuite.js) right after the “Testing the obvious” describe:
      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:

      • $T.createComponent() instantiates a Lightning component. The first parameter is the component name. The second parameter is an optional list of attribute values. The third parameter specifies whether the test requires the component to be rendered.
      • Once the component is instantiated, you can use the cmp object as usual to get attribute values, find DOM elements, etc.
    3. Run the test suite again by accessing https://<BASE_URL>/c/myTestApp.app or by typing the following command:
sfdx force:lightning:test:run -a myTestApp.app

Scenario 2: Verify data binding

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.

  1. Create a component called componentWithDataBinding implemented as follows:
    <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>
    
  2. Add the following test to your test suite:
    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);
          });
       });
    });
    
  3. Run the test suite again.

Scenario 3: Verify method invocation

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.

  1. Create a component called componentWithMethod implemented as follows:
    componentWithMethod.cmp

    <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);
        }
    })
    
  2. Add the following test to your test suite:
    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.

  3. Run the test suite again.

Scenario 4: Verify application event

In this example, we verify that a component listening to an application event works as expected when the application event is fired.

  1. Create an Application event named myAppEvent implemented as follows:
    <aura:event type="APPLICATION">
        <aura:attribute name="message" type="String" />
    </aura:event>
    
  2. Create a component named componentListeningToAppEvent implemented as follows:
    componentListeningToAppEvent.cmp

    <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"));
        }
    })
    
  3. Add the following test to your test suite:
    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);
                });
        });
    });
    
  4. Run the test suite again.

Scenario 5: Verify server method invocation (not recommended)

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.

  1. Create an Apex class called AccountController implemented as follows:
    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;
        }
    }
    
  2. Create a Lightning Component named accountList implemented as follows:
    accountList.cmp

    <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);
    	}
    })
    
  3. Add the following test to your test suite:
    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.

  4. Run the test suite again.

Scenario 6: Verify mocked server method invocation (recommended)

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.

  1. Add the following test to your test suite:
    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);
                });
        });
    });
    
  2. Run the test suite again.

Other Testing Scenarios

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.

Summary

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.

Additional Resources

Leave your comments...

Getting Started with the Lightning Testing Service