Testing TypeScript with Intern

This post has been updated to cover Intern 3.4 and TypeScript 2.3

Intern is a popular JavaScript testing framework, because of its extensive, modular feature set. While Intern is primarily known for testing JavaScript applications, it is also an excellent option for authoring tests with TypeScript. And Intern’s support for source maps makes it easy to track issues back to your original TypeScript source files.

Getting started

To get started, we will test a simple ToDoMVC example application. We’ll begin with specifying a directory structure and providing some example source code.

Directory structure

For the examples in this post, you should use the following directory structure:

todomvc/
node_modules/intern/
src/todo/
model/
SimpleTodoModel.js
SimpleTodoModel.js.map
SimpleTodoModel.ts
tests/
intern.js
intern.js.map
intern.ts
functional/
all.js
all.js.map
all.ts
SimpleTodoModel.js
SimpleTodoModel.js.map
SimpleTodoModel.ts
unit/
all.js
all.js.map
all.ts
SimpleTodoModel.js
SimpleTodoModel.js.map
SimpleTodoModel.ts
/typings/intern/intern.d.ts

One of the main challenges with test framework setup are meta problems, including source code directories and configuration. If you’re using a different directory structure, you may need to modify your Intern and/or TypeScript configuration.

Source code

For this example, we have not authored a new example application in TypeScript, but assume we have one of the many ToDoMVC applications and we want to test it. The example tests here will focus on testing the SimpleTodoModel as we have already created examples in the intern-examples repo for this and many other popular ToDoMVC example applications.

Unit tests

Because typical Intern tests are normally authored in JavaScript, most Intern examples are authored with ES5. To help you author TypeScript tests, we will compare JavaScript vs. TypeScript authored tests.

For the SimpleTodoModel example, the JavaScript test source code is:

[javascript]
define([
'intern!object',
'intern/chai!assert',
'todo/model/SimpleTodoModel'
], function (registerSuite, assert, SimpleTodoModel) {
registerSuite({
name: 'SimpleTodoModel',
'default data': function () {
var emptyModel = new SimpleTodoModel();
assert.strictEqual(emptyModel.get('id'), 'todos-dojo',
'Id should default to "todos-dojo"');
assert.strictEqual(emptyModel.get('todos').length, 0,
'Todos array should default to an empty array.');
assert.strictEqual(emptyModel.get('incomplete'), 0,
'Incomplete count should default to 0.');
assert.strictEqual(emptyModel.get('complete'), 0,
'Incomplete count should default to 0.');
}
});
[/javascript]

In this example, we’re creating a set of assertions inside a test suite for our ToDoMVC application.

To accomplish the same with TypeScript, we can leverage TypeScript’s simplified syntax for object literals. And if your application code is authored in TypeScript, you will no longer need to write unit tests that check data types, since the compiler will enforce this for you. You also get the many other advantages any code base receives when using TypeScript (interfaces, enhancements to the language, etc.).

Here is the same example test suite authored in TypeScript:

[typescript]
import * as assert from 'intern/chai!assert';
import * as registerSuite from 'intern!object';
// Assume that we now have a version of our model in TypeScript
import * as SimpleTodoModel from 'todo/model/SimpleTodoModel';

registerSuite({
name: 'SimpleTodoModel',
// Assume we have a promises interface defined
'default data'() {
var emptyModel = new SimpleTodoModel(),
id:string = emptyModel.get('id'),
length:number = emptyModel.get('todos').length,
incomplete:number = emptyModel.get('incomplete'),
complete:number = emptyModel.get('complete');
assert.strictEqual(id, 'todos-dojo',
'Id should default to "todos-dojo"');
assert.strictEqual(length, 0,
'Todos array should default to an empty array.');
assert.strictEqual(incomplete, 0,
'Incomplete count should default to 0.');
assert.strictEqual(complete, 0,
'Incomplete count should default to 0.');
}
});[/typescript]

How does this work?

It is important to remember that just like TypeScript applications, you will only be able to run tests when they have been converted to JavaScript. Fortunately the TypeScript compiler will very easily compile to JavaScript in several different module formats, including AMD or UMD, along with source maps. As such, test modules are imported with the TypeScript import statement in your code and then converted to AMD for testing.

Configure and compile

To test with Intern, you specify a configuration file, which can be done with either JavaScript or TypeScript.

To compile your source code and tests, you’ll add the relevant compiler settings to your tsconfig.json:

[typescript]
{
"version": "2.3.4",
"compilerOptions": {
"declaration": false,
"experimentalDecorators": true,
"module": "umd",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitThis": true,
"outDir": "_build/",
"removeComments": false,
"sourceMap": true,
"strictNullChecks": true,
"target": "es5"
},
"include": [
"./src/**/*.ts",
"./tests/**/*.ts",
"./typings/index.d.ts"
]
}
[/typescript]

You’ll also want to include the relevant type related packages in your package.json, and any specific versions of typings that might be needed in typings.json.

TypeScript typings for Intern

The Intern 3.4 release includes its own typings which are available when installing Intern. The forthcoming Intern 4 release is authored in TypeScript.

Functional tests

Functional tests work in the same manner as unit tests, but leverage Intern’s Leadfoot implementation of the WebDriver API. With JavaScript, a ToDoMVC functional test example for submitting a form looks like this:

[javascript]
define([
'intern!object',
'intern/chai!assert',
'require'
], function (registerSuite, assert, require) {
var url = '../../index.html';
registerSuite({
name: 'Todo (functional)',
'submit form': function () {
return this.remote
.get(require.toUrl(url))
.findById('new-todo')
.click()
.pressKeys('Task 1')
.pressKeys('\n')
.pressKeys('Task 2')
.pressKeys('\n')
.pressKeys('Task 3')
.getProperty('value')
.then(function (val) {
assert.ok(val.indexOf('Task 3') > -1, 'Task 3 should remain in the new todo');
});
}
});
});
[/javascript]

Rewritten in TypeScript, the test is:

[typescript]
import * as assert from 'intern/chai!assert';
import * as registerSuite from 'intern!object';

var url = '../../index.html';
registerSuite({
name: 'Todo (functional)',
'submit form'() {
return this.remote
.get(require.toUrl(url))
.findById('new-todo')
.click()
.pressKeys('Task 1')
.pressKeys('\n')
.pressKeys('Task 2')
.pressKeys('\n')
.pressKeys('Task 3')
.getProperty('value')
.then(function (val:string) {
assert.ok(val.indexOf('Task 3') > -1, 'Task 3 should remain in the new todo');
});
}
});
[/typescript]

TypeScript + Intern examples

If you need more inspiration in creating tests with TypeScript and Intern, the following projects contain unit and functional test examples:

Caveats

Most previous limitations and caveats have been resolved with recent releases of Intern and TypeScript. Most remaining complexity around testing with TypeScript are being resolved with the Intern 4 release.

Testing alias modules

A common pattern with Intern is to create modules that simply include all of your tests to run inside a module. This makes it easy to maintain a list of tests outside of your Intern configuration file, a good DRY practice. However, if you import a module and never reference it, the TypeScript compiler skips it. If you are writing a module that is simply a list of all of your other modules to test, this won’t work. The workaround is to simply refer to the module within your list of tests.

For example, if you were to do this normally with a JavaScript set of tests:

[javascript]
// all.js
define([
'./model/SimpleTodoModel',
'intern/node_modules/dojo/has!host-browser?./store/LocalStorage',
'intern/node_modules/dojo/has!host-browser?./form/CheckBox'
], function () {});
[/javascript]

So then with TypeScript, you would need to do the following:

[typescript]
// all.ts
/// <amd-dependency path="intern/node_modules/dojo/has!host-browser?./store/LocalStorage" />
/// <amd-dependency path="intern/node_modules/dojo/has!host-browser?./form/CheckBox" />
import * as SimpleTodoModel from 'todo/model/SimpleTodoModel';
SimpleTodoModel;
[/typescript]

Notice the single reference to SimpleTodoModel immediately after importing the module. As an aside, in this example, we're assuming that we would rewrite the ToDoMVC example in TypeScript, but some of its dependencies are still authored in JavaScript. It may be more useful to view a complete example of this test module pattern.

Non-TypeScript paths

TypeScript does not concern itself with the paths to JavaScript AMD modules as it does not load or read them. TypeScript depends on the definitions in the *.d.ts files. It is up to you to make sure your AMD paths are correct in your application when you run it, but this is irrelevant for TypeScript compilation. Note that relative paths in the TypeScript compiler are calculated relative to wherever the *.d.ts file is, not the actual JavaScript AMD module files.

Further reading

Learning more

There is much more detail to authoring TypeScript tests, but the main takeaway is that you simply author tests with valid TypeScript, and compile to AMD for testing. If you’re not sure where to start with Intern, or you need some help making your TypeScript source code more testable, or want assistance in defining a testing strategy for your organization, SitePen can help!

Getting Help With Typescript and Intern

SitePen’s TypeScript for the Enterprise Developer and Intern workshops are a quick way to jumpstart your journey into the modern era!

SitePen Support. Receive timely answers and relevant code examples from early adopters and active users of TypeScript and the creators of Intern.

Let’s talk about how we can help your organization benefit from the use of TypeScript in your next project.

Have a question? We’re here to help! Get in touch and let’s see how we can work together.

Originally posted on SitePen.com

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.