-
Notifications
You must be signed in to change notification settings - Fork 308
Writing Tests with Intern
When writing JavaScript tests, it is common to write them according to a specific testing API that defines Suites, Tests, and other aspects of testing infrastructure. These testing APIs are known as test interfaces. Intern currently comes with support for 3 different test interfaces: TDD, BDD, and object. Internally, all interfaces generate the same testing structures, so you can use whichever interface you feel matches your preference and coding style. Examples of each tests using each of these interfaces can be found below.
A test needs a way to verify some logic about the target being tested, such as whether or not a given variable is truthy. This is known as an assertion, and forms the basis for software testing. Intern supports extensible assertions via the Chai Assertion Library. The assert and expect/should assertion interfaces are exposed via the following modules, and should be required and used in your tests:
intern/chai!assert
intern/chai!expect
intern/chai!should
As of Intern 2, it is also possible to use the Chai as Promised plugin when writing functional tests by loading the Chai as Promised plugin into Chai. First, install Chai as Promised to your project using npm (it does not come with Intern), then activate it like this:
define([ 'intern/chai!', 'intern/dojo/node!chai-as-promised' ], function (chai, chaiAsPromised) {
chai.use(chaiAsPromise);
chai.should();
});
Once Chai as Promised has been activated, it becomes available to the functional testing API (this.remote
).
Asynchronous testing in Intern is based on promises. You may either return a promise from a test function (convenient for interfaces that already support promises) or call this.async
from within a test function to enable asynchronous testing.
If your test returns a promise (any object with a then
function), it is understood that your test is asynchronous. Resolving the promise indicates a passing test, and rejecting the promise indicates a failed test. The test will also fail if the promise is not fulfilled within the timeout
of the test (the default is 30 seconds; set this.timeout
to change the value).
All tests have a skip
method that can be used to skip the test if it should not be executed for some reason. For example:
define([
'intern!object',
'intern/chai!assert'
], function (registerSuite, assert) {
registerSuite({
name: 'skip demo',
'skip test': function () {
if (typeof window === 'undefined') {
this.skip('Browser-only test');
}
/* … */
}
});
});
Note that calling this.skip
halts execution of the rest of the function, so it is not necessary to return after calling it.
All tests have an async
method that can be used to get a Deferred object for asynchronous testing. After calling this method, Intern will assume your test is asynchronous, even if you do not return a promise from your test function. (If you do return a promise, the returned promise takes precedence over the promise generated by this.async
.)
async
returns a Deferred object that can be used to resolve the test once it has completed. In addition to the standard Deferred API, this Deferred object has two additional methods:
-
Deferred#callback(function):function
: This method wraps a callback and returns a function that will resolve the Deferred automatically when the function is called, so long as the callback does not throw any errors. If the callback throws an error, the Deferred will be rejected with that error instead. This is the most common way to complete an asynchronous test. -
Deferred#rejectOnError(function):function
: This method works similarly toDeferred#callback
, except it only implements rejection behavior. In other words, the newly created function will reject the Deferred when there is an error, but will not resolve it when there is no error. This is useful when working with nested callbacks where only the innermost callback should resolve the Deferred but a failure in any of the outer callbacks should reject it.
The async
method accepts two optional arguments. The first argument, timeout
, will set the timeout of your test in milliseconds. If not provided, this defaults to 30 seconds. The second argument, numCallsUntilResolution
, specifies how many times dfd.callback
should be called before actually resolving the promise. This defaults to 1. numCallsUntilResolution
is useful in rare cases where you may have a callback that will be called several times and the test should be considered complete only on the last invocation.
A basic asynchronous test using this.async
looks like this (object style):
define([
'intern!object',
'intern/chai!assert',
'request'
], function (registerSuite, assert, request) {
registerSuite({
name: 'async demo',
'async test': function () {
var dfd = this.async(1000);
request('http://example.com/test.json').then(dfd.callback(function (data) {
assert.strictEqual(data, 'Hello world!');
}), dfd.reject.bind(dfd));
}
});
});
In this example, an XHR call is performed. When the call is completed successfully, the data is checked to make sure it is correct. If the data is correct, dfd
will be resolved; otherwise, it will be rejected (because assert.strictEqual
will throw an error). If the call fails, dfd.reject
is called.
In addition to regular unit tests, Intern supports a type of testing that can simulate user interaction with DOM elements, known as functional testing. Functional tests are slightly different from normal unit tests because they are executed remotely from the test runner, whereas unit tests are executed directly on the browser under test. In a functional test, a remote
object is exposed that has methods for interacting with a remote browser environment. The general flow of a functional test should be as follows:
Because the test code isn’t exposed to this remote environment at all, this HTML page should include script tags for all necessary JavaScript. Note that if a functional test needs to explicitly wait for certain widgets or elements on this HTML page to be rendered (or some other condition) before proceeding, the pollUntil promise helper can be used. This helper waits until a global variable is non-null before continuing with execution, and errors out if an optional timeout is exceeded.
this.remote
.get(require.toUrl('./SomeTest.html'))
.then(pollUntil('return window.ready;', 5000));
The remote
object is a Leadfoot Command object.
this.remote
.get(require.toUrl('./fixture.html'))
.findById('operation')
.click()
.type('hello, world')
.end()
Just like unit tests, functional tests support extensible assertions via the Chai Assertion Library. The assert and expect/should assertion interfaces are exposed via the intern/chai!assert
, intern/chai!expect
, and intern/chai!should
modules. See the full Chai API documentation for more information.
this.remote
.setFindTimeout(10000)
.findById('result')
.text()
.then(function (resultText) {
assert.equal(resultText, 'hello world',
'When form is submitted, operation should complete successfully');
});
CommonJS code, including Node.js built-ins, can be loaded as an AMD dependency from within Node.js using the dojo/node
AMD plugin that comes with Intern:
define([
'intern!object',
'intern/chai!assert',
'intern/dojo/node!path'
], function (registerSuite, assert, path) {
registerSuite({
name: 'path',
'basic tests': function () {
path.join('a', 'b');
// ...
}
});
});
If you are attempting to test non-AMD code that is split across multiple JavaScript files which must be loaded in a specific order, use the intern/order
plugin instead of specifying those files as direct dependencies in order to ensure they load correctly:
define([
'intern!object',
'intern/chai!assert',
'intern/order!../jquery.js',
'intern/order!../plugin.jquery.js'
], function (registerSuite, assert) {
registerSuite({
name: 'plugin.jquery.js',
'basic tests': function () {
jQuery('<div>').plugin();
// ...
}
});
});
You can also try use.js.
(Of course, it is strongly recommended that you upgrade your code to use AMD so that this is not necessary.)
define([
'intern!bdd',
'intern/chai!expect',
'../Request'
], function (bdd, expect, Request) {
with (bdd) {
describe('demo', function () {
var request,
url = 'https://github.com/theintern/intern';
// before the suite starts
before(function () {
request = new Request();
});
// before each test executes
beforeEach(function () {
request.reset();
});
// after the suite is done
after(function () {
request.cleanup();
});
// multiple methods can be registered and will be executed in order
// of registration
after(function () {
if (!request.cleaned) {
throw new Error(
'Request should have been cleaned up after suite execution.');
}
// these methods can be made asynchronous as well by returning
// a promise
});
// asynchronous test for Promises/A-based interfaces
it('should demonstrate a Promises/A-based asynchronous test', function () {
// `getUrl` returns a promise
return request.getUrl(url).then(function (result) {
expect(result.url).to.equal(url);
expect(result.data.indexOf('next-generation') > -1).to.be.true;
});
});
// asynchronous test for callback-based interfaces
it('should demonstrate a callback-based asynchronous test', function () {
// test will time out after 1 second
var dfd = this.async(1000);
// dfd.callback resolves the promise as long as no errors are
// thrown from within the callback function
request.getUrlCallback(url, dfd.callback(function () {
expect(result.url).to.equal(url);
expect(result.data.indexOf('next-generation') > -1).to.be.true;
});
// no need to return the promise; calling `async` makes the test async
});
// nested suites work too
describe('xhr', function () {
// synchronous test
it('should run a synchronous test', function () {
expect(request.xhr).to.exist;
});
});
});
}
});
define([
'intern!tdd',
'intern/chai!assert',
'../Request'
], function (tdd, assert, Request) {
with (tdd) {
suite('demo', function () {
var request,
url = 'https://github.com/theintern/intern';
// before the suite starts
before(function () {
request = new Request();
});
// before each test executes
beforeEach(function () {
request.reset();
});
// after the suite is done
after(function () {
request.cleanup();
});
// multiple methods can be registered and will be executed in order
// of registration
after(function () {
if (!request.cleaned) {
throw new Error(
'Request should have been cleaned up after suite execution.');
}
// these methods can be made asynchronous as well by returning
// a promise
});
// asynchronous test for Promises/A-based interfaces
test('#getUrl (async)', function () {
// `getUrl` returns a promise
return request.getUrl(url).then(function (result) {
assert.equal(result.url, url,
'Result URL should be requested URL');
assert.isTrue(result.data.indexOf('next-generation') > -1,
'Result data should contain term "next-generation"');
});
});
// asynchronous test for callback-based interfaces
test('#getUrlCallback (async)', function () {
// test will time out after 1 second
var dfd = this.async(1000);
// dfd.callback resolves the promise as long as no errors are
// thrown from within the callback function
request.getUrlCallback(url, dfd.callback(function () {
assert.equal(result.url, url,
'Result URL should be requested URL');
assert.isTrue(result.data.indexOf('next-generation') > -1,
'Result data should contain term "next-generation"');
});
// no need to return the promise; calling `async` makes the
// test async
});
// nested suites work too
suite('xhr', function () {
// synchronous test
test('sanity check', function () {
assert.ok(request.xhr,
'XHR interface should exist on `xhr` property');
});
});
});
}
});
define([
'intern!object',
'intern/chai!assert',
'../Request'
], function (registerSuite, assert, Request) {
var request,
url = 'https://github.com/theintern/intern';
registerSuite({
name: 'demo',
// before the suite starts
setup: function () {
request = new Request();
},
// before each test executes
beforeEach: function () {
request.reset();
},
// after the suite is done
teardown: function () {
request.cleanup();
if (!request.cleaned) {
throw new Error(
'Request should have been cleaned up after suite execution.');
}
},
// asynchronous test for Promises/A-based interfaces
'#getUrl (async)': function () {
// `getUrl` returns a promise
return request.getUrl(url).then(function (result) {
assert.equal(result.url, url,
'Result URL should be requested URL');
assert.isTrue(result.data.indexOf('next-generation') > -1,
'Result data should contain term "next-generation"');
});
},
// asynchronous test for callback-based interfaces
'#getUrlCallback (async)': function () {
// test will time out after 1 second
var dfd = this.async(1000);
// dfd.callback resolves the promise as long as no errors are
// thrown from within the callback function
request.getUrlCallback(url, dfd.callback(function () {
assert.equal(result.url, url,
'Result URL should be requested URL');
assert.isTrue(result.data.indexOf('next-generation') > -1,
'Result data should contain term "next-generation"');
});
// no need to return the promise; calling `async` makes the test
// async
},
// nested suites work too
'xhr': {
// synchronous test
'sanity check': function () {
assert.ok(request.xhr,
'XHR interface should exist on `xhr` property');
}
}
});
});
define([
'intern!object',
'intern/chai!assert',
'../Request',
'require'
], function (registerSuite, assert, Request, require) {
var request,
url = 'https://github.com/theintern/intern';
registerSuite({
name: 'demo',
'submit form': function () {
return this.remote
.get(require.toUrl('./fixture.html'))
.findById('operation')
.click()
.type('hello, world')
.end()
.findById('submit')
.click()
.end()
.setFindTimeout(Infinity)
.findById('result')
.setFindTimeout(0)
.text()
.then(function (resultText) {
assert.ok(resultText.indexOf(
'"hello, world" completed successfully') > -1,
'On form submission, operation should complete successfully');
});
}
});
});