Intern

Intern is a popular JavaScript testing framework with an extensive feature set. While Intern has traditionally been focused on testing applications written in standard JavaScript, it has also had great support for TypeScript. With version 4, Intern has been completely rewritten in TypeScript, allowing it to provide a more seamless testing experience for TypeScript projects.

Getting started

We will be looking at tests for a simple TodoMVC example application. The source code for this example can be found in the intern-examples repo.

Directory structure

The application and tests use the following directory structure. Putting TypeScript in a src directory is typical for TS projects. We put tests in a separate directory that mirrors the structure of the source; this can simplify source code management since testing resources aren’t mixed in with application code.

todomvc/
    intern.json
    src/
        model/
            todo.ts
        routers/
            router.ts
        ...
    tests/
        functional/
            Todo.ts
        unit/
            model/
                todo.ts
            routers/
                router.ts

Source code

The application itself is a simple Bootstrap + jQuery TodoMVC app, written as a set of TypeScript modules. Modules are easily tested since they can be loaded individually in test suites, and most module loading systems allow for module dependencies to be mocked or stubbed. Intern is well suited for testing both modular and non-modular code; it does not require or even assume that a module loader is present in the target environment. Since this example is written using modules, a module loader will be present; in this case, SystemJS.

Unit tests

TypeScript offers some obvious advantages for unit tests, the most signficant being that the TypeScript compiler will catch API misuses, both with Intern and with the application itself (for applications written in TypeScript).

The TypeScript unit test suite for the Todo model looks like:

import Todo from 'src/models/todo';

const { registerSuite } = intern.getInterface('object');
const { assert } = intern.getPlugin('chai');

let todo;

registerSuite('todo model', {
    beforeEach() {
        todo = new Todo();
        todo.sync = () => Promise.resolve({});
    },

    tests: {
        defaults() {
            assert.isFalse(
                todo.get('completed'),
                'A Todo model should default the completed property to false'
            );
            assert.strictEqual(
                todo.get('title'),
                '',
                'A Todo model should default the title property to an empty string'
            );
        },

        toggle() {
            todo.toggle();
            assert.isTrue(
                todo.get('completed'),
                'Completed property should switch to true after being toggled for the first time'
            );

            todo.toggle();
            assert.isFalse(
                todo.get('completed'),
                'Completed property should switch back to false after being toggled again'
            );
        }
    }
});

This test suite imports the Todo model class and runs tests on model instances. Before each test is run, a new Todo instance is created and its sync method is stubbed to prevent the model from trying to make ajax requests. The tests simply verify that a Todo instance has a certain default state, and that its toggle method behaves as expected.

The suite registration function and assert module are both fully typed, so TypeScript will alert a test writer if registerSuite is called with an improperly formatted object, or if an unknown assertion is made. Also, note the use of the intern global. The TypeScript compiler knows about this global because of the "types": ["intern"] compiler option in the test tsconfig.json file. It is also possible to import the suite registration function and assertion library using standard import statements.

Note the path used to import the Todo model: “src/models/todo” rather than the actual relative path from the test module to the application module: “../../../src/models/todo”. This works because of a baseUrl setting in the test tsconfig.json file. By default, importing a non-relative path causes TypeScript to look for a package in the project root, which is the location of the tsconfig.json file being used for compilation. However, that file has a baseUrl value of “..”, which sets the base directory to the project root. From there, the src package is visible.

Test writers must ensure that the import paths used in tests will both build with TypeScript and work at runtime, because the the TypeScript import process is distinct from that used by the runtime loader (SystemJS in our case). In this case it will work as-is because SystemJS will be loading both tests and application modules from the same base path, so a module path like “src/models/todo” in a test will map directly to the corresponding application module at runtime. Even if that weren’t the case, most runtime loaders are easily configured to look for packages in arbitrary locations.

Functional tests

Functional tests look very similar to unit tests, but leverage Intern’s Leadfoot implementation of the WebDriver API. A functional test suite that tests Todo item entry looks like:

const { registerSuite } = intern.getInterface('object');
const { assert } = intern.getPlugin('chai');

import keys from '@theintern/leadfoot/keys';

registerSuite('Todo (functional)', {
    'submit form'() {
        return this.remote
            .get('index.html')
            .findByCssSelector('.new-todo')
            .type('Task 1')
            .type(keys.RETURN)
            .type('Task 2')
            .type(keys.RETURN)
            .type('Task 3')
            .getSpecAttribute('value')
            .then(value => {
                assert.ok(
                    value.indexOf('Task 3') > -1,
                    'Task 3 should remain in the new todo'
                );
            });
    }
});

One improvement gained by writing Intern 4 in TypeScript is improved typings. For example, in Intern 3 a test function would have needed an explicit typing for the this parameter in order for TypeScript to know that the remote property was available. With Intern 4, the registerSuite typings are detailed enough that TypeScript can infer the proper type for this in a test automatically.

Building the tests

Tests are built separately from the application code, using a tsconfig.json file in the tests directory that extends the project’s original tsconfig.json. Using a separate TS config file for tests allows them to include test-specific settings and typings while still using most of the settings from the main project.

{
    "extends": "../tsconfig.json",
    "compilerOptions": {
        "baseUrl": "..",
        "types": [ "intern" ]
    },
    "include": [
        "./**/*.ts"
    ]
}

Tests can be compiled with tsc -p tests. Both tests and application code can be built in the example project by running npm run build.

Configuring and running Intern

While Intern 3 used an executable module for configuration, Intern 4 is configured with a JSON file, typically named intern.json and located in the project root directory. This file tells Intern where to find tests and what environments to run tests in. When testing TypeScript projects, there are two key concepts to keep in mind:

  1. Properties that refer to files must refer to JavaScript (i.e., compiled) files, not the original TS files,
  2. If tests and/or application code is built as modules (e.g., AMD or CommonJS), a module loader will need to be used in the browser
{
    // Suites refer to the built JavaScript files
    "suites": "tests/unit/**/*.js",
    "functionalSuites": "tests/functional/**/*.js",
    "coverage": "src/**/*.js",
    "browser": {
        // In the browser, Intern's SystemJS loader plugin is used
        // to load SystemJS. Note that the systemjs package must be
        // installed.
        "loader": "systemjs",

        // The application's SystemJS config (the same one loaded in the normal
        // application's index.html) is loaded here.
        "plugins": { "script": "src/config.js", "useLoader": true }
    }
}

Note the loader property. This property tells Intern to use its built-in “systemjs” script to load the SystemJS loader, and to use it to load test suites (and plugins that have their useLoader property set to true). Without this property Intern would load modular code as simple JavaScript scripts; with no access to the import or require functions.

There are a couple of ways to run Intern, depending on what version of npm is installed on a system:

  1. With npm < 5, run node_modules/.bin/intern
  2. With npm >= 5, you can also run npx intern

As with most npm-based projects, you can also run this examples tests with npm test.

TypeScript + Intern examples

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

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 JavaScript 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!