Skip to content

Writing Tests with Intern

csnover edited this page Sep 12, 2014 · 2 revisions

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.

Assertions

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

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.

Returning a promise

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).

Calling this.skip() (2.1+)

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.

Calling this.async()

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 to Deferred#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.

Functional testing

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:

1. Load an HTML page into the remote environment

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));

2. Use the methods available on the remote object to interact with the remote context.

The remote object is a Leadfoot Command object.

this.remote
	.get(require.toUrl('./fixture.html'))
	.findById('operation')
		.click()
		.type('hello, world')
	.end()

3. Make assertions just like regular unit testing.

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');
	});

Testing non-AMD code

CommonJS code

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');
			// ...
		}
	});
});

Browser code

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.)

Example Tests

BDD

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;
				});
			});
		});
	}
});

TDD

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');
				});
			});
		});
	}
});

Object

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');
			}
		}
	});
});

Functional

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');
				});
		}
	});
});