Unit testing aura components with Lightning Testing Service and Mocha

When thinking about good quality software we must always have very present that tests automation is a must. There is a full list of tiers and tools that can be used to ensure that your software behaves as expected, bugs typically introduced by refactoring are prevented, and interconnected systems behave well together, as for example your code together with a Salesforce upgrade. In this post I want to focus on how to write unit tests for Lightning Components using Lightning Testing Service (LTS).

Lightning Testing Service is an open source that includes a Jasmine and a Mocha wrapper that will allow you to write your own unit test for Lightning Components Javascript code, although you can create your own wrappers too. It also include utility objects that allow you to instantiate the components in test context, fire application events and other stuff.

When I first started working with LTS of tests, I couldn’t find too much documentation. Also I was new to Javascript unit testing, and it was hard for me, so I thought it was a good idea to publish a blog post explaining how to unit test the most typical use cases. 

First you should be able to install LTS in your org, create a standalone app to include the tests that you want to run, and create a static resource to store your tests following  the instructions in the LTS repo or the official Salesforce Developers blog. This can be done with or without SFDX, but I strongly recommend you to use SFDX as it will be much easier to work. You can follow

Once you have done all of this, let’s explore the typical structure for a test file:

describe('c:myCmp', () => {
    afterEach(() => {
        $T.clearRenderedTestComponents();
    });

    describe('My test suite', () => {
        // Unit tests here
    });
});

As you can see, we can describe several test suites for a component. Typically each test suite will contain several unit tests. Notice that each test renders its components into the same div, so we need to clear that div at the end of each test.

Normally a unit test has three parts:

GIVEN: we setup the initial conditions for the test. Typically we create the component and setup some of its attributes.

WHEN: we emulate that something happens, an action. This can be for example invoking an aura method, setting an attribute, performing an action on a DOM element, etc.

THEN: we check what our component did in response to that action, its behavior, not its implementation!. Typically, if the component has fired an event, retrieved something from the server, changed something in the DOM, etc.

Let’s dig into several use cases for the actions (WHENs) and the behavior (THENs) that you can emulate and check in a lightning component unit test.

In the examples I will use Mocha JS (JS test framework), Sinon JS (to create spies, stubs and mocks) and Chai JS (to perform assertions).

The WHENs: which actions can be emulated?

To show examples of how to test the different WHENs use cases I have created the next aura components:

whenUseCases.cmp

<aura:component>
    <aura:attribute name="attr1" type="String"/>
    <aura:attribute name="attr2" type="String"/>

    <aura:handler name="init" value="{!this}" action="{!c.onInit}"/>
    <aura:handler name="change" value="{!v.attr1}" action="{!c.onAttr1Change}"/>

    <aura:handler name="myCmpEvent" event="c:cmpEvent" action="{!c.onCmpEvent}"/>
    <aura:handler event="c:appEvent" action="{!c.onAppEvent}"/>

    <aura:method name="methodCall" action="{!c.onMethodCall}">
        <aura:attribute name="param1" type="String" required="true"/>
    </aura:method>

    <c:whenUseCasesChild aura:id="child-cmp"/>

    <a aura:id="my-link" onclick="{!c.onInteractWithDOM}">Click me</a>
</aura:component>

whenUseCasesController.js

({
    // In the examples, the THEN will be just setting attr2, for simplicity
    onInit : function(cmp, event, helper) {
        cmp.set('v.attr2', 'cmp initialized');
    },
    onAttr1Change : function(cmp, event, helper) {
        cmp.set('v.attr2', 'attr1 changed');
    },
    onMethodCall : function(cmp, event, helper) {
        cmp.set('v.attr2', 'method invoked');
    },
    onInteractWithDOM: function(cmp, event, helper) {
        cmp.set('v.attr2', 'link clicked');
    },
    onCmpEvent: function(cmp, event, helper) {
        cmp.set('v.attr2', 'cmp event fired');
    },
    onAppEvent: function(cmp, event, helper) {
        cmp.set('v.attr2', 'app event fired');
    }
})

whenUseCasesChild.cmp

<aura:component>
    <aura:registerEvent name="myCmpEvent" type="c:cmpEvent"/>
</aura:component>

As you can see, the controller methods are very simple, I  just set a component attribute in all the cases, because my purpose here is just to show you how to perform the actions, the WHENs. We will have another section to show you how to check the component behavior, the THENs.

1. Emulate that a component is initialized

it('Should set attr2 on init', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        // GIVEN

        // WHEN
        // We don't have to do anything! Just creating the component
        // will fire any logic in the onInit function

        // THEN
        expect(cmp.get('v.attr2')).to.equal('cmp initialized');
    });
});

This use case is very simple, we just create the component.

2. Emulate that we set a component attribute

it('Should set attr2 on attr change', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        // GIVEN
        const attr1 = 'my attr 1';

        // WHEN
        cmp.set('v.attr1',attr1);

        // THEN
        expect(cmp.get('v.attr2')).to.equal('attr1 changed');
    });
});

In this case we simply have to use cmp.set(‘v.attr1’,…);, as we would do in non test code.

3. Emulate that an aura method as been invoked

it('Should set attr2 on method call', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        // GIVEN
        const attr1 = 'my attr 1';

        // WHEN
        cmp.methodCall(attr1);

        // THEN
        expect(cmp.get('v.attr2')).to.equal('method invoked');
    });
});

Again, we simply have to call the method using cmp.methodCall(…); as we would do in non test code.

4. Emulate that a DOM element has changed, being clicked etc.

it('Should set attr2 on link click', () => {
    // We need to specify we want the DOM to be rendered
    // to the createComponent function
    return $T.createComponent('c:whenUseCases', {}, true).then((cmp) => {
        // GIVEN

        // WHEN
        // As cmp.find returns an aura cmp, we need to obtain
        // the correspondant DOM element from it
        const link = cmp.find('my-link').getElement();
        link.click();

        // THEN
        expect(cmp.get('v.attr2')).to.equal('link clicked');
    });
});

In this case, we have to get the DOM element using getElement(), and then use the DOM API in the way that we want to. Using getElement() is necessary because, even if you are writing html tags directly in your code, the framework creates an aura:html component behind the scenes, which is what cmp.find(…) returns, and a DOM element linked to it, which is what auraElem.getElement() returns.

But remember the DOM element is not exactly the one everybody knows, but a secure version of it. Check the Locker Service API documentation  documentation to know the differences.

5. Emulate that a component event is fired

it('Should set attr2 when cmp event is fired', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        // GIVEN
        const attr1 = 'my attr 1';

        // WHEN
        // As it is a cmp event, it must be fired by a child component in order to be listened by the parent
        const event = cmp.find('child-cmp').getEvent('myCmpEvent');
        event.setParams({cmpAttr1: attr1});
        event.fire();

        // THEN
        expect(cmp.get('v.attr2')).to.equal('cmp event fired');
    });
});

Note that to test this, I had to create a child component that fires the event, as remember that component events just bubble up in the hierarchy.

Again, we just emulate the event in the same way that it is fired in non test code.

6. Emulate that an application event is fired

it('Should set attr2 when app event is fired - returning a promise', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        // GIVEN
        const attr1 = 'my attr 1';

        // WHEN
        $T.fireApplicationEvent("c:appEvent", {cmpAttr1: attr1});

        // THEN
        expect(cmp.get('v.attr2')).to.equal('app event fired');
    });
});

This time we have to use a method that resides in the $T namespace, that has been implemented in the LTS for you to be able to fire application events in test context.

The THENs: which expected behaviors can we check?

To show examples of how to test the different THENs use cases I have created the next aura components:

thenUseCases.cmp

<aura:component controller="ThenUseCasesController">
    <aura:attribute name="attr1" type="String"/>

    <aura:registerEvent name="myCmpEvent" type="c:cmpEvent"/>
    <aura:registerEvent name="myAppEvent" type="c:appEvent"/>

    <!-- In the examples, the WHEN will be just calling aura methods, for simplicity -->
    <aura:method name="setAttribute" action="{!c.onSetAttribute}"/>
    <aura:method name="setAttributeAsync" action="{!c.onSetAttributeAsync}"/>
    <aura:method name="returnSomething" action="{!c.onReturnSomething}">
        <aura:attribute name="param1" type="String" required="true"/>
    </aura:method>
    <aura:method name="modifyDOMElement" action="{!c.onModifyDOMElement}"/>
    <aura:method name="fireCmpEvent" action="{!c.onFireCmpEvent}"/>
    <aura:method name="fireAppEvent" action="{!c.onFireAppEvent}"/>
    <aura:method name="invokeApex" action="{!c.onInvokeApex}"/>
    <aura:method name="callChildMethod" action="{!c.onCallChildMethod}"/>

    <a aura:id="my-link">Click me</a>

    <c:thenUseCasesChild aura:id="child-cmp"/>
</aura:component>

thenUseCasesController.js

({
    onSetAttribute : function(cmp, event, helper) {
        cmp.set('v.attr1', 'attribute set');
    },
    onSetAttributeAsync : function(cmp, event, helper) {
        setTimeout(() => {
            cmp.set('v.attr2', 'attribute set async');
        }, 500);
    },
    onReturnSomething : function(cmp, event, helper) {
        var params = event.getParam('arguments');
        if (params) {
            return 'You passed: ' + params.param1;
        }
    },
    onModifyDOMElement : function(cmp, event, helper) {
        cmp.find('my-link').getElement().innerHTML = 'Changed!';
    },
    onFireCmpEvent : function(cmp, event, helper) {
        const evt = cmp.getEvent('myCmpEvent');
        evt.setParams({cmpAttr1: 'my attr 1'});
        evt.fire();
    },
    onFireAppEvent : function(cmp, event, helper) {
        var evt = $A.get('e.c:appEvent');
        evt.setParams({cmpAttr1: 'my attr 1'});
        evt.fire();
    },
    onInvokeApex : function(cmp, event, helper) {
        var action = cmp.get('c.executeApex');
        action.setParams({ message : 'hi!' });
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === 'SUCCESS') {
                cmp.set('v.attr1', response.getReturnValue());
            }
            else if (state === 'ERROR') {
                cmp.set('v.attr1', response.getError());
            }
        });

        $A.enqueueAction(action);
    },
    onCallChildMethod : function(cmp, event, helper) {
        cmp.find('child-cmp').methodCall('goodbye!');
    }
})

ThenUseCasesController.cls

public with sharing class ThenUseCasesController
{
    @AuraEnabled
    public static String executeApex(String message)
    {
        return 'Apex invoked! ';
    }
}

thenUseCasesChild.cmp

<aura:component>
    <aura:method name="methodCall" action="{!c.onMethodCall}">
        <aura:attribute name="param1" type="String" required="true"/>
    </aura:method>
</aura:component>

thenUseCasesChildController.js

({
    onMethodCall : function(cmp, event, helper) {
        // Do whatever
    }
})

In these use cases, the starting point will be always calling a method, for simplicity, as my purpose is to test what the component does in response to any arbitrary action (that you have been able to emulate thanks to the previous section!).

Bear in mind for these examples I have created the next constants, to make reading easier. Don’t worry too much about them for now, I will explain what they are used for in the examples in which they are used:

const expect = chai.expect;
const sandbox = sinon.sandbox.create();

1. Check that an attribute has been set

it('Should set attr1 on method call', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN

        // WHEN
        cmp.setAttribute();

        // THEN
        expect(cmp.get('v.attr1')).to.equal('attribute set');
    });
});

We get the component attribute value using cmp.get(‘v.attr1’), as it would be done in non test code, and we use the expect method from chai to perform an assertion. 

2. Check that an attribute has been set after some time (async)

it('Should set attr1 on method call - async', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN

        // WHEN
        cmp.setAttributeAsync();

        // THEN
        $T.waitFor(() => {
            return cmp.get('v.attr1') === 'attribute set async';
        }).then(() => {
            done();
        }).catch((e) => {
            done(e);
        });
    });
});

LTS provides a method that allows you to wait for something to happen. This will be specially useful when you need to wait for async operations, for example, modifications of the DOM.

3. Check the return value for an aura method

it('Should return correct value on method call', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const attr1 = 'my attr 1';

        // WHEN
        const returnValue = cmp.returnSomething(attr1);

        // THEN
        expect(returnValue).to.equal('You passed: ' + attr1);
    });
});

Just perform an assertion over the returned value!

4. Check that a DOM element has been manipulated

it('Should modify DOM element on method call', () => {
    // We need to specify we want the DOM to be rendered
    // to the createComponent function
    return $T.createComponent('c:thenUseCases', {}, true).then((cmp) => {
        // GIVEN

        // WHEN
        cmp.modifyDOMElement();

        // THEN
        expect(cmp.find('my-link').getElement().innerHTML).to.equal('Changed!');
    });
});

As in the WHEN’s case, we need to use getElement() to get the DOM element and then use the LockerService API to get the content of the element or anything else that we want to check.

5. Check that a component event has been fired by a child component

it('Should fire component event on method call', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const fakeEventHandler = sinon.spy();
        cmp.addEventHandler('myCmpEvent', fakeEventHandler);

        // WHEN
        cmp.fireCmpEvent();

        // THEN
        expect(fakeEventHandler.callCount).to.equal(1);

        const auraEvent = fakeEventHandler.getCall(0).args[0];
        const cmpAttr1 = auraEvent.getParam('cmpAttr1');

        expect(cmpAttr1).to.equal('my attr 1');
    });
});

And it must be a child component, because component events just bubble up in the hierarchy! 

In this case we need to add a fake event handler to the parent, to detect that the event has been fired. For that we will use a sinon spy. A spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls. So, we will be able to assert, for example, that the fake handler has been called once, and with the correct attributes.

6. Check that an application event has been fired

it('Should fire app event on method call', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const fakeEventHandler = sinon.spy();
        cmp.addEventHandler('myAppEvent', fakeEventHandler);

        // WHEN
        cmp.fireAppEvent();

        // THEN
        expect(fakeEventHandler.callCount).to.equal(1);

        const auraEvent = fakeEventHandler.getCall(0).args[0];
        const cmpAttr1 = auraEvent.getParam('cmpAttr1');

        expect(cmpAttr1).to.equal('my attr 1');
    });
});

We can test it exactly in the same way than the previous case.

7. Check that an Apex  method has been invoked and the response has been successful

it('Should invoke Apex on method call - success response', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const dummyData = new Date().toUTCString();
        const dummyResponse = {
            getState: () => { return 'SUCCESS'; },
            getReturnValue: () => { return dummyData; }
        };

        const enqueueActionStub = sandbox.stub($A, 'enqueueAction').callsFake((action) => {
            const cb = action.getCallback('SUCCESS');
            cb.fn.apply(cb.s, [dummyResponse]);
        });

        // WHEN
        cmp.invokeApex();

        // THEN
        expect(enqueueActionStub.callCount).to.equal(1);

        const action = enqueueActionStub.getCall(0).args[0];
        const params = action.getParams();

        expect(action.getName()).to.equal('executeApex');
        expect(params.message).to.equal('hi!');
        expect(cmp.get('v.attr1')).to.equal(dummyData);
    });
});

In this case, we are creating a sinon stub to emulate the server response. Stubs are spies with pre-programmed behavior. This is, we can define a different implementation for the function that we stub.

Don’t worry too much about the word sandbox, it just makes easier to clean up stubs.

In this case, we are stubbing the $A.enqueueAction function to we will have a implementation that just gets the ‘SUCCESS’ callback of the action.

You will be probably a bit confused by these two lines:

const cb = action.getCallback('SUCCESS');
cb.fn.apply(cb.s, [dummyResponse]);

This is the way recommended by Salesforce to emulate that the success callback function is called (JS apply does this) passing in the dummy response we have defined.

Sadly, this is not an elegant way, as it asumes we know the internal implementation of the Action object in aura framework. If you are curious about it, here you can see that “fn” is a property that stores the success callback function itself, and “s” a property that stores the scope that we passed in when we called action.setCallback in the real implementation.

Worth to say that the Apex controller method should have their own Apex unit tests too.

8. Check that an Apex  method has been invoked and the response has been error

it('Should invoke Apex on method call - error response', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const dummyData = new Date().toUTCString();
        const dummyResponse = {
            getState: () => { return 'ERROR'; },
            getError: () => { return dummyData; }
        };

        const enqueueActionStub = sandbox.stub($A, 'enqueueAction').callsFake((action) => {
            const cb = action.getCallback('ERROR');
            cb.fn.apply(cb.s, [dummyResponse]);
        });

        // WHEN
        cmp.invokeApex();

        // THEN
        expect(enqueueActionStub.callCount).to.equal(1);

        const action = enqueueActionStub.getCall(0).args[0];
        const params = action.getParams();

        expect(action.getName()).to.equal('executeApex');
        expect(params.message).to.equal('hi!');
        expect(cmp.get('v.attr1')).to.equal(dummyData);
    });
});

As you can see, the test is very similar to the previous case, but this time we will stub the error callback.

9. Check that a child component’s method has been called X times, with Y parameters

it('Should call child cmp method passing a parameter', () => {
    return $T.createComponent('c:thenUseCases', null).then((cmp) => {
        // GIVEN
        const spy = sinon.spy(cmp.find('child-cmp'), 'methodCall');

        // WHEN
        cmp.callChildMethod();

        // THEN
        expect(spy.calledOnce).to.be.true;
        expect(spy.getCall(0).args[0]).to.equal('goodbye!');
    });
});

In this case we are using a sinon spy to check how many times a method has been called and with which arguments. As you can see, this case is very similar to the case in which we check that a fake event handler has been invoked because that event was fired. 

BUT WAIT… I HAVE SEEN A DIFFERENT SYNTAX SOMEWHERE!

If you have been working with LTS before you may have seen a different syntax. The it() callback function can either return a promise (what we have been doing in all the examples) or call the done() function. This means that these two tests are equivalent:

// Returning a promise

it('Should set attr2 on init - Returning a promise', () => {
    return $T.createComponent('c:whenUseCases', null).then((cmp) => {
        expect(cmp.get('v.attr2')).to.equal('cmp initialized');
    });
});

// Calling done()

it('Should set attr2 on init - calling done()', (done) => {
    $T.createComponent('c:whenUseCases', null).then((cmp) => {
        expect(cmp.get('v.attr2')).to.equal('cmp initialized');
        done();
    }).catch(e => {
        done(e);
    });
});

It is a matter of taste which one to use.

WHICH THINGS CANNOT BE EMULATED / TESTED?

  • You won’t be able to emulate a client side action (function defined in the controller) or a javascript function defined in the helper, unless there is an aura:method that exposes it. You won’t be able to check these have been invoked either. Because of this, is really important to architect your components in a modular way so that they are testable.
  • You can emulate the same actions / behavior checks over a child component retrieved with component.find(…), for example, set an attribute from the child component, check that a method of the child component has been called, etc. However, if the child component lives in a different namespace (eg: lightning base components), you will be able to interact only with the component public API (attributes and methods).
  • You cannot emulate LEX events that are not supported in Lightning Out (as force:showToast), as LTS executes in Lightning Out context, so the events won’t exist there. You cannot test these events have been fired either. As a workaround, embed the code that fires the event in a method an spy it.

Finally I would like to say that Sinon (and any other mocking framework in general) is a big world, you can do much more than I have done here, as check exceptions have been fired by Javascript functions, use matchers for matching attributes (eg: any string, any number…), etc. I recommend you to have a deeper look at it.

What about the future? On one hand, it is not weird to think that Salesforce will include LTS natively in Salesforce orgs soon to run aura components tests, as the Apex tests runner is currently, so that we don’t have to worry about setting it up.

On the other hand, with the release of LWC (Lightning Web Components), Javascript unit tests will be run locally (using Jest.js). I have given it a try, and sadly Salesforce has not provided yet a mock implementation for base components, so, you won’t be able to work with base components that use slots (eg: lightning-layout, lightning-layout-item) in test context unless you create your own mock implementation. I also think that Salesforce will probably release this at some point. 

You can check the full code of the examples in my git repo.

One thought on “Unit testing aura components with Lightning Testing Service and Mocha

Add yours

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: