Testing Applications and Libraries

Contents

This guide describes how to test libraries and applications using the Enonic XP test framework. It is based on JUnit and uses a mix of Java and JavaScript. Tests must be bootstrapped with Java and then you can write all your tests using pure JavaScript.

The test framework requires Enonic XP 7.0 or newer

All code samples for this Guide is available on Github: https://github.com/enonic/guide-testing-apps.

Add Dependency

First you will need to add the Enonic XP testing dependency to your project.

dependencies {
  testImplementation 'com.enonic.xp:testing:7.11.3' (1)
}
1 The testing framework ships with the XP core, replace version number with the version of XP you are compiling for.

Testing a Library

Let’s start with a simple library which is just a plain JavaScript file with some exports. This library here has a function returning a Fibonacci-sequence.

src/main/resources/lib/fibonacci.js
function fibonacci(n) {
    var fib = [0, 1];
    for (var i = fib.length; i < n; i++) {
        fib[i] = fib[i - 2] + fib[i - 1];
    }

    return fib;
}

// Export the function.
exports.fibonacci = fibonacci;

We can write a couple of tests for this. Let’s test the function for a couple of inputs.

src/test/resources/lib/fibonacci-test.js
var lib = require('./fibonacci');
var t = require('/lib/xp/testing');

exports.testSequence4 = function () {
    var result = lib.fibonacci(4);
    t.assertJson([0, 1, 1, 2], result);
};

exports.testSequence6 = function () {
    var result = lib.fibonacci(6);
    t.assertJson([0, 1, 1, 2, 3, 5], result);
};

Every exported function that is prefixed with test is executed as a separate test. You can also export before and after if you need to execute some logic before or after each test.

But, to be able to execute this test you will also need to write a little "bootstrap" code in Java. Here’s how this will look for this particular test:

src/test/java/com/enonic/guide/FibonacciTest.java
package com.enonic.guide;

import com.enonic.xp.testing.ScriptRunnerSupport;

public class FibonacciTest
    extends ScriptRunnerSupport
{
    @Override
    public String getScriptTestFile()
    {
        return "/lib/fibonacci-test.js";
    }
}

Testing a Controller

Testing controllers is identical to what is described in the previous section. For this example we have a simple service that serves a GET request.

src/main/resources/services/hello/hello.js
exports.get = function (req) {
    return {
        body: 'Hello ' + (req.params.name || 'World'),
        contentType: 'text/plain'
    };
};

Let’s write a test that tests two conditions: one where the parameter is not set and another one where the parameter is set.

src/test/resources/services/hello/hello-test.js
var service = require('./hello');
var t = require('/lib/xp/testing');

exports.testParam = function () {
    var result = service.get({
        params: {
            name: 'Donald'
        }
    });

    t.assertEquals('Hello Donald', result.body);
    t.assertEquals('text/plain', result.contentType);
};

exports.testNoParam = function () {
    var result = service.get({
        params: {}
    });

    t.assertEquals('Hello World', result.body);
    t.assertEquals('text/plain', result.contentType);
};

Again, to execute the test we need a little bit of Java.

src/test/java/com/enonic/guide/HelloServiceTest.java
package com.enonic.guide;

import com.enonic.xp.testing.ScriptRunnerSupport;

public class HelloServiceTest
    extends ScriptRunnerSupport
{
    @Override
    public String getScriptTestFile()
    {
        return "/services/hello/hello-test.js";
    }
}

Mocking Services

Sometimes it can be useful to mock certain libraries so it’s easier to test. Let’s say we depend on a library that gives us the time of day. To fix the return value in our tests we need to mock this library.

src/main/resources/lib/time.js
exports.now = function () {
    return new Date().toISOString();
};

Our service that uses the time is like this:

src/main/resources/services/clock/clock.js
var time = require('/lib/time');

exports.get = function () {
    return {
        body: 'Time is ' + time.now()
    };
};

To be able to test this we need to mock our time library in our test so we can return a fixed result.

src/test/resources/services/clock/clock-test.js
var t = require('/lib/xp/testing');

t.mock('/lib/time.js', { (1)
    now: function () {
        return '2017-08-01T12:13:24.000Z';
    }
});

exports.testClock = function () {
    var service = require('./clock'); (2)

    var result = service.get();
    t.assertEquals('Time is 2017-08-01T12:13:24.000Z', result.body);
};
1 Mock the time library so we can control the output.
2 Clock service uses the time library in code. It will get the mocked version.

In a slightly bigger scenario, we might want to mock a module and have the same function mock-return different subsequent values for different scenarios and tests. There is a trick to this:

To have a mocked function return different values in different tests (within the same test module, myModuleTest.js below), repeated t.mock calls must use the same object instance (mockedImportedFuncs below) across the calls to hold the mocked functions, and replace the functions (mutate the holding object) before re-calling t.mock with the holding object. Also, t.mock must have been called before importing the mock-consuming module (myModule.js below).

For example:

src/main/resources/lib/valueCreator.js:
exports.getValue = function() {
    return Math.random();
}

The output of getValue is used in:

src/main/resources/services/myModule/myModule.js:
const valueCreator = require('/lib/valueCreator.js');

exports.get = function() {
    const value = valueCreator.getValue();
    if (value < .5) {
        return "Low";
    } else {
        return "High";
    }
}

valueCreator.getValue() is too unpredictable to test how it affects the behavior in myModule.get, so that is what we want to mock when testing it different scenarios. Here, we’ll make the holding object mockedImportedFuncs global, and make a mockGetValue function to wrap the mocking for each test - re-using mockedImportedFuncs across t.mock calls:

src/test/resources/services/myModule/myModuleTest.js:
const t = require('/lib/xp/testing');

const mockedImportedFuncs = {};
t.mock('/lib/valueCreator', mockedImportedFuncs);

// Import the module under test AFTER running t.mock once:
const service = require('./myModule');

function mockGetValue(mockedReturn) {
    mockedImportedFuncs.getValue = function() {
        return mockedReturn;
    }
    t.mock('/lib/valueCreator', mockedImportedFuncs);
}

exports.testMocked1 = () => {
    mockGetValue(0);
    const highOrLow = service.get();
    t.assertEquals("Low", highOrLow);
}
exports.testMocked2= () => {
    mockGetValue(1);
    const highOrLow = service.get();
    t.assertEquals("High", highOrLow);
}
exports.testMockHalf = () => {
    mockGetValue(0.5);
    const highOrLow = service.get();
    t.assertEquals("High", highOrLow);
}

Contents

Contents