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.
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.
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:
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.
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.
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.
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.
exports.now = function () {
return new Date().toISOString();
};
Our service that uses the time is like this:
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.
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:
exports.getValue = function() {
return Math.random();
}
The output of getValue
is used in:
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:
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);
}