Skip to content

Latest commit

 

History

History
384 lines (312 loc) · 14.2 KB

Writing-Tests-with-Intern.md

File metadata and controls

384 lines (312 loc) · 14.2 KB

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 various 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

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.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 is used to wrap a callback function and will resolve the Deferred automatically when it is called, so long as the callback function does not throw any errors. If the callback function 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 the same as Deferred#callback, except it will only reject the Deferred when there is an 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, which will set the timeout of your test in milliseconds. If not provided, this defaults to 30 seconds. The second argument, numCallsUntilResolution, which allows you to specify how many times dfd.callback should be called before actually resolving the promise. If not provided, 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 context.

Because the actual test code isn't exposed to this remote client at all, this html page should include script tags for all necessary JavaScript. Note that if the 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 waitForCondition method can be used. This method waits until a global variable becomes truthy before continuing with execution, and errors out if an optional timeout is exceeded.

this.remote
	.get(require.toUrl('./SomeTest.html'))
	.waitForCondition('ready', 5000);

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

The remote object corresponds to the standard WebDriver API with a fluid, promises-wrapped WD.js. See methods available for functional testing.

this.remote
	.get(require.toUrl('./fixture.html'))
	.elementById('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 various Chai 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
	.waitForElementById('result')
	.text()
	.then(function (resultText) {
		assert.equal(resultText, 'hello world', 'When form is submitted, operation should complete successfully');
	});

See the full Chai API documentation for more information on each module.

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

New in Version 1.1

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'))
				.elementById('operation')
					.click()
					.type('hello, world')
				.end()
				.elementById('submit')
					.click()
				.end()
				.waitForElementById('result')
				.text()
				.then(function (resultText) {
					assert.ok(resultText.indexOf('"hello, world" completed successfully') > -1, 'When form is submitted, operation should complete successfully');
				});
		}
	});
});