Skip to main content

Setting the execution context of JavaScript functions (a.k.a. setting the "this" variable) in tests

If you are not practising TDD (Test Driven Development) stop reading now and leave! You are persona non grata! :-)

Just kidding... Read on and you might find enlightmnet...

Testing pure functions in JavaScript is quite straighforward. You just have to provide the arguments required and you are done. No, complex dependencies, no DOM dependencies. Pure fun(ctions)! For example, for the given function

const square = function(num)
{
    return num * num;
};

testing is as easy as:

describe('#square', function()
{
    it('should return the square of the provided number', function()
    {
        expect(square(10)).toBe(100);
    });
});

Things get a little complicated, though, when you are trying to test more complex stuff, that include DOM manipulation, DOM events, etc.

Imagine for exaple, that you have a function that contextually provides a DOM element (depending on the "this" context) which you use as your base element, for doing various DOM stuff. Check this out:

const getRootDomElement = function(selector)
{
    if (isNotDefined(selector))
    {
        selector = '.company-row';
    }
    return $(this).closest(selector);
};

Apparently, the value that this function will return depends on the "this" context/variable, which is set when (in this case) a checkbox is clicked:

$('input.cover-sidebar').on('click', function()
{
    const rootDomElement = getRootDomElement.call(this);
});

Now, imagine that you want to test that the getRootDomElement() function works correctly. How would you go about testing this?

Testing environment

First of all, you need the right testing environment. Obviously, you need a DOM to interact with. So, just using Jasmine (or any other testing framework) may not be enough.

The way I do it, is by using Karma.js (for all my tests, not only the ones that require a DOM, since Karma uses real browsers and therefore, real runtime environments).

So, after setting up your Karma testing environment, something like the following would be appropriate:

describe('getRootDomElement(domSelector)', function()
{
    beforeEach(function()
    {
        const fixture = `<div id="fixture"> <div class="company-row"> <div class="box sidebar-covers">
        <input class="cover-sidebar" id="cover_1" name="covers" type="checkbox"
        value="1" checked="checked"> <input class="cover-sidebar" id="cover_2"
        name="covers" type="checkbox" value="2" checked> <input class="cover-sidebar"
        id="cover_3" name="covers" type="checkbox" value="3"> <input class="cover-sidebar"
        id="cover_4" name="covers" type="checkbox" value="4"> </div>
        </div> </div>`;

        $('body').append(fixture);
    });

    afterEach(function()
    {
        $('#fixture').remove();
    });

    it('should get the right rootDomElement, depending on the domSelector provided', function()
    {
        const context = document.getElementsByClassName('cover-sidebar')[0];

        const defaultRootDomElement = getRootDomElement.call(context);
        const $coversCheckboxesFromDefaultRootElement = getCheckedCheckboxes(defaultRootDomElement, 'input.cover-sidebar');

        const customSelectorRootDomElement = getRootDomElement.call(context, '.sidebar-covers');
        const $coversCheckboxesFromCustomSelectorRootDomElement = getCheckedCheckboxes(customSelectorRootDomElement, 'input.cover-sidebar');

        const defaultCheckboxesFirstElementId = $($coversCheckboxesFromDefaultRootElement[0]).attr('id');
        const customCheckboxesFirstElementId = $($coversCheckboxesFromCustomSelectorRootDomElement[0]).attr('id');

        expect($coversCheckboxesFromDefaultRootElement.length).toBe(2);
        expect($coversCheckboxesFromCustomSelectorRootDomElement.length).toBe(2);

        expect(defaultCheckboxesFirstElementId).toBe('cover_1');
        expect(customCheckboxesFirstElementId).toBe('cover_1');
        expect(defaultCheckboxesFirstElementId).toBe(customCheckboxesFirstElementId);
    });
});

The point that we are very interested in, is this:

const context = document.getElementsByClassName('cover-sidebar')[0];
const defaultRootDomElement = getRootDomElement.call(context);

Instead of relying on the checkbox click to capture the "this" context, we get it beforehand and we pass it to the getRootDomElement() function. Notice that in this particular case, we pass the first element ([0]) from the elements that getElementsByClassName returns.

The whole testing setup may require a little more work, but, trust me, it pays off.

Seeing green will make the fear go away. For good...

Happy testing!