Streamlining UI Integration Tests with Promises

Writing integration tests for your UIs in JavaScript can get messy fast. You start off with some nice simple tests, and before you know it you’re in callback hell. By taking a step back and better structuring your tests and utilising the power of Promises though, you can avoid this. This post outlines how we tackled this problem and have made it quicker, easier, and cleaner to write integration tests in JavaScript.

For the purpose of this post, I’ll be using Mocha as my test runner, and you can assume waitForElement asynchronously waits for an element to be visible in the DOM. You should be easily able to adapt this to your favourite test runner. If you’re new to promises, I recommend you take a look at Jake Archibalds primer on Promises on HTML5 Rocks.

Let’s dive in. If you’ve ever written integration tests for some asynchronous JavaScript, you’ve probably written something like this.

1
2
3
4
5
6
7
8
9
10
11
12
it('Valid search should return results', function (done) {
waitForElement('#searchField', function (searchField) {
searchField.value = 'mySearch';
var document.getElementById('searchButton');
searchButton.click();
waitForElement('#results', function (results) {
// ASSERTIONS
done();
});
});
});

This is a simple example, but it could get worse as you write more complicated tests. As with most asynchronous code, we can improve the readability by using promises. With very little work, we can change our code to use promises.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('Valid search should return results', function (done) {
waitForElement('#searchField')
.then(function (searchField) {
searchField.value = 'mySearch';
var document.getElementById('searchButton');
searchButton.click();
return Promise.resolve();
})
.then(function () {
return waitForElement('#results');
})
.then(function (results) {
// ASSERTIONS
})
.then(done);
});

Ok, it’s a little better. It gets past the nesting of callbacks, and each one does a specific function now, but it still looks a little messy. We’ll also end up rewriting most of this code over and over again.

For this, we want to simplify repeated actions. We can start by abstracting our view. This will give us more readable code, as well as making it easier to make small changes, whether it’s to IDs or class names. We can also create a few utility methods for common interactions.

ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var ViewModel = {
/**
* Waits for the search field
*
* @method getSearchField
* @returns {Promise} promise
*/
getSearchField: function () {
return waitForElement('#searchField');
},
/**
* Waits for the search button
*
* @method getSearchButton
* @returns {Promise} promise
*/
getSearchButton: function () {
return waitForElement('#searchButton');
},
/**
* Waits for the results
*
* @method getResults
* @returns {Promise} promise
*/
getResults: function () {
return waitForElement('#results');
}
};
CommonActions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var CommonActions = function () {
/**
* Returns a function to be used as a test step to set the value of the element from the previous test step.
*
* @method setValue
* @param {String} val
* @returns {Function} testStep
*/
setValue: function (val) {
return function (el) {
el.value = val;
return Promise.resolve();
}
},
/**
* Clicks the provided element
*
* @method click
* @param {HTMLElement} el
* @returns {Promise} promise
*/
click: function (el) {
el.click();
return Promise.resolve();
}
};

Once we have this set up, we can further streamline our test.

1
2
3
4
5
6
7
8
9
10
11
12
it('Valid search should return results', function (done) {
ViewModel.getSearchField()
.then(CommonActions.setValue('mySearch'))
.then(ViewModel.getSearchButton)
.then(CommonActions.click)
.then(ViewModel.getResults)
.then(function (results) {
// ASSERTIONS
return Promise.resolve();
})
.then(done);
});

This is much more readable than our first test, as it basically reads as plain English. However, I wanted more flexibility, so I decided to take this further. I wrote a few functions to easier handle the lifecycle of the test. It might seem a little over the top at first, but bear with me. The comments explain what it does.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Ensure that the argument provided is usable in a chain of promises regardless of type.
*
* @method promisify
* @param {*} resp
* @returns {Promise} promise
*/
function promisify(resp) {
return resp && resp.constructor === Promise ? resp : Promise.resolve(resp);
}
/**
* Check if argument is a function
*
* @method isFunction
* @param {*} functionToCheck
* @returns {Boolean} isFunction
*/
function isFunction(functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}
/**
* Runs a set of test steps passed in as an array.
*
* @method runTestSteps
* @param {Array} testSteps
* @return {Promise} lastPromise
*/
function runTestSteps(testSteps) {
var lastPromise = Promise.resolve(); // Just to chain off of
testSteps.forEach(function (testStep) {
lastPromise.catch(function (e) {
// Unfortunately need to do this to get a proper stack trace in mocha
requestAnimationFrame(function () {
throw e;
});
});
lastPromise = lastPromise.then(function () {
if (isFunction(testStep)) {
// If testStep is a function, process as a normal promise
return promisify(testStep.apply(this, arguments));
} else if (testStep && testStep.constructor === Array) {
// Recurse if it's an array
return runTestSteps(testStep);
} else {
// Just resolve with the value of testStep isn't a function or array
return Promise.resolve(testStep);
}
});
});
return lastPromise;
}

Now that we’ve set up this boilerplate, we can define our tests with a simple array of test steps. We can also get rid of any “return Promise.resolve()”s we have, and can even call methods directly in our test steps that just return regular values. We can also bundle tests steps into functions for reuse.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Get the set of test steps to perform a search
*
* @method searchTestSteps
* @param {String} searchTerm
* @returns {Array} testSteps
*/
function searchTestSteps(searchTerm) {
return [
ViewModel.getSearchField,
CommonActions.setValue(searchTerm),
ViewModel.getSearchButton,
CommonActions.click
];
}
it('Valid search should return results', function (done) {
runTestSteps([
search('validSearch'),
ViewModel.getResults,
function (results) {
// ASSERTIONS
},
done
]);
});

Now that we have everything nicely abstracted, we can very easily add another test for an invalid search.

1
2
3
4
5
6
7
8
9
10
it('Invalid search should return results', function (done) {
runTestSteps([
search('invalidSearch'),
ViewModel.getResults,
function (results) {
// ASSERTIONS
},
done
]);
});

And that’s about it. I hope this helps you write simpler, quicker UI integration tests. Any questions or feedback, sound off in the comments, or hit me up on Twitter @DanielBrierton.