In this tutorial we will walk through how to set up Intern and how to write tests and run tests. This repository contains a basic Hello World demo “application” that we’ll be using as an example to build on. In order to complete this tutorial, you will need the following:
Once you have all the necessary prerequisites, download the demo application by cloning this repository and installing the dependencies:
git clone https://github.com/theintern/intern-tutorial.git
cd intern-tutorial
npm install
The application itself consists of a basic HTML page and a single “app” package
written in TypeScript. Several npm
scripts have been provided to simplify the
building and testing processes:
npm run compile
- runs the TypeScript compiler oncenpm run compile:watch
- runs the TypeScript compiler in watch mode (runs the
compiler, waits for changes, and re-compiles when changes are detected)npm run copy
- copies any assets from src
to _dist/src
npm run copy:watch
- watches for changes to assets in src
and copies files
to _src/dist
when changes are detectednpm run build
- runs npm run compile
and npm run copy
in parallelnpm run build:watch
- runs npm run compile:watch
and npm run copy:watch
in parallelnpm test
- builds the application and runs InternIn order for the demo application to work properly during the tutorial, make
sure that you access it using a real web server. Like most applications, it will
not work from a file:
URL due to cross-protocol browser security
restrictions.
Intern can test all sorts of things:
Intern is minimally prescriptive and enforces only a basic set of best practices designed to ensure your tests stay maintainable over time. Its extensible architecture allows you to write custom test interfaces, executors, and reporters to influence how your tests run and easily integrate with your existing coding environment.
Unlike most other testing systems, Intern also supports two different types of testing: unit testing and functional testing. Unit testing works by executing code directly and inspecting the result, such as calling a function and then checking that it returns an expected value. Functional testing works by mimicking user interaction with a browser by issuing commands through a WebDriver server (an executable that lets testing tools interact with a browser).
This is a powerful notion: Intern allows us to test code with regular unit tests, but also allows us to test functionality by mimicking user interaction with the browser.
Intern is distributed as an npm package so
it can be easily added as a dependency to any JavaScript project. We’ll install
Intern using npm install --save-dev
so that npm adds it automatically as a
development dependency to application’s
package.json:
npm install --save-dev intern
We also need to tell TypeScript to load Intern’s and SystemJS’s type definitions
by default. This ensures that typings for global variables provided by Intern
and SystemJS will be available in tests. Add the following to the
"compilerOptions"
object in tsconfig.json
:
"types": [
"intern",
"systemjs"
]
That’s it! Installation is complete.
Intern needs to be configured so it can find and run our tests. This is done by
creating an Intern configuration file named intern.json
at the root of our
project:
{
"browser": {
"loader": {
"script": "systemjs"
},
"plugins": {
"script": "_dist/src/system.config.js",
"useLoader": true
}
},
"environments": ["node", { "browserName": "chrome" }]
}
This configuration tells Intern that, in the browser, we want to use SystemJS to load modules, and that we want to load a plugin to configure SystemJS. The plugin needs to have access to the SystemJS loader, so we set the "useLoader" flag to true. Without the "useLoader" flag the plugin would be loaded before the external loader, meaning it wouldn’t have access to SystemJS.
The configuration also tells Intern that, in addition to running our unit tests in Node.js, we want to run our tests in Chrome. You can find more information about possible configuration options in the Configuration section of the Intern documentation.
We’ll be doing a little more configuration shortly when we start adding tests, but for now, we already have a complete configuration. You can verify that everything is working by running Intern:
npm test
It should output:
No unit test coverage for node
node: 0 passed, 0 failed
Now that we’ve configured Intern, we need to create a test module which will contain the actual tests for our application.
There are a variety of syntaxes used to write unit tests, and Intern comes with built-in support for several of them, including TDD, BDD, and object. In this tutorial, we will use the TDD syntax, but this is an individual preference. All of these interfaces support the same basic functionality, so pick whichever one you think is the clearest when you start writing your own tests!
Before getting any further into writing tests, we need to take a moment to review the terminology that is used by Intern:
assert.isTrue(someVariable)
)These pieces can be visualized in a hierarchy, like this:
Test modules are typically split up so that there’s one test module for each corresponding code module being tested. First, create a new subdirectory for storing all of the unit tests:
mkdir -p tests/unit
We have one code module in our demo app (app/hello
), so we’ll create a new
unit test module at intern-tutorial/tests/unit/hello.ts
and put the following
boilerplate into it:
const { suite, test } = intern.getPlugin('interface.tdd');
const { assert } = intern.getPlugin('chai');
import { greet } from '../../src/app/hello';
This bit of code loads the suite
and test
functions from the TDD test
interface, the assert
function of Chai, and
the greet
function we want to test.
Now that the basics of our hello
test module are in place, the next step is to
use suite
to register a test suite and test
to register a test case for our
app. We’ll start by testing the greet
function.
Looking at the source code for app/hello
, we can see that when greet
is
called it will return the string "Hello, world!"
if no name is passed, or
"Hello, <name>!"
if a name is passed. We need to make sure we test both of
these code branches. If we’ve done it right, our test code will end up looking
something like this:
const { suite, test } = intern.getPlugin('interface.tdd');
const { assert } = intern.getPlugin('chai');
import { greet } from '../../src/app/hello';
suite('hello', () => {
test('greet', () => {
assert.strictEqual(
greet('Murray'),
'Hello, Murray!',
'greet should return a greeting for the person named in the first argument'
);
assert.strictEqual(
greet(),
'Hello, world!',
'greet with no arguments should return a greeting to "world"'
);
});
});
Note: This example test uses assert.strictEqual
, which is just one of many
available assertions. For a complete list of available methods, see the
Chai documentation.
In this test module, we’ve registered a new suite for our hello
module and
named it “hello”, written a new test case for the greet
method and named it
“greet”, and added two assertions: one where we call greet
and pass an
argument, and one where we call greet
without any argument. If either of these
assertions fails, they will throw an error and the test case will be considered
failed at that point.
Each of our assertions also contains a message that describes what logic the
assertion is actually checking. Similar to good code comments that describe
why a piece of code exists, these messages are used to describe the intent of
the code being checked rather than simply describing the assertion. For
instance, “Calling greet('Murray') should return "Hello, Murray!"” would be a
bad assertion message because it just describes what the assertion is doing,
rather than describing the desired outcome. With the message we’ve used in the
code above, if the greet
function were changed in the future to return
"Hi, <name>!"
instead, it would be clear that the test itself needed to be
updated because the code still fulfills the described business logic. Similarly,
if the method were changed to return "You suck, <name>!"
instead, it would
then be clear that the application code was updated incorrectly.
Now that we’ve created our first test module, we need to update the TypeScript
config to actually compile the test. Add a glob for the tests directory to the
“include“ property in tsconfig.json
:
"include": [
"src/**/*.ts",
"tests/**/*.ts"
]
Note how the suite file accesses getPlugin
on a global intern
variable. This
variable will be created when Intern is loaded, but the typings won't know about
it without an additional config update. Add a types
property to the
compilerOptions
section of the tsconfig.json
:
"compilerOptions": {
"types": ["intern"]
}
This property tells the Typescript compiler to load Intern's types implicitly
when compiling the tests, which ensures that Typescript knows about the global
intern
variable.
The final step when writing a new test module is to add the compiled
module‘s path to our configuration file so that it is loaded when we run Intern.
To do this, add a suites
property to the top-level object of intern.json
with the string "_dist/tests/unit/hello.js"
:
"suites": "_dist/tests/unit/hello.js",
Now if we go back and run the same npm test
command from the end of Step 2, we
should see our tests running (and passing) in both Node.js and Chrome:
Listening on localhost:9000 (ws 9001)
Tunnel started
✓ node - hello - greet (0.001s)
No unit test coverage for node
node: 1 passed, 0 failed
‣ Created remote session chrome 59.0.3071.115 on MAC (714dfe2b-ebc0-4249-b235-a3756d004fc8)
✓ chrome 59.0.3071.115 on MAC - hello - greet (0.001s)
No unit test coverage for chrome 59.0.3071.115 on MAC
chrome 59.0.3071.115 on MAC: 1 passed, 0 failed
TOTAL: tested 2 platforms, 2 passed, 0 failed
These same tests can be run directly within a Web browser by running
npm test serveOnly
and navigating to
http://localhost:9000/__intern/index.html
.
Functional tests are different from unit tests in that they mimic user
interaction by sending commands to browsers using an external server instead of
running directly in the environment being tested. This enables us to generate
real DOM events and test UI interactions just like a real user, with no
JavaScript security sandbox limitations. As well as enabling testing of
sandbox-restricted actions like file uploads, functional testing also allows us
to test interactions that span multiple pages and interactions with third party
sites (like OAuth authorization flows). Our demo app contains an HTML file with
a basic form that should display a greeting using app/hello.greet
. For this
tutorial, we’ll simulate a user filling out a form and clicking a button to
submit it in order to verify this page works as expected.
Intern’s functional testing is based on the standard WebDriver protocol and comes with built-in support for remote testing services as well as self-hosted WebDriver servers. The rest of this tutorial assumes you are using BrowserStack.
To get started, create a new directory to hold the functional tests (in order to
differentiate them from our normal unit tests) at
intern-tutorial/tests/functional
:
mkdir -p tests/functional
Next, create a test module at intern-tutorial/tests/functional/index.ts
with
the following boilerplate:
const { suite, test, before } = intern.getPlugin('interface.tdd');
const { assert } = intern.getPlugin('chai');
suite('index', () => {
before(() => {});
test('greeting form', () => {});
});
Just like the unit test we created before, we’re using the object test interface and assert-style assertions. However, instead of loading any application code directly, we’ll be using WebDriver to load our page in the browser.
To facilitate functional testing, an object is passed to every lifecycle and
test function which has a remote
property. The remote
property exposes an
object that provides an interface for interacting with the remote browser
environment. Using the methods on remote
, we can load a Web page, interact
with it, and retrieve data from it to assert that our actions caused the
expected result. Since all calls to the remote browser are asynchronous, all
methods of the remote
object return promises. This allows us to either chain
commands (like jQuery) and retrieve results using standard promises-style then
calls or use async/await to write synchronous-looking tests. When we make a
call, it is enqueued and executed once all the previous commands have completed.
If this description is a little confusing, don’t worry — it should be clearer
once we look at some code.
Looking at the HTML page at index.html
, we can see that it consists of a
simple form with a single input. It loads app/main
which sets up our event
listeners and adds a CSS class of “loaded” to the body element. We want to make
sure this form works properly by testing interaction like a real user: focusing
the input, typing a string, and clicking submit. We can then verify that the
greeting was properly updated. Once finished, this test will look something like
this:
const { suite, test, before } = intern.getPlugin('interface.tdd');
const { assert } = intern.getPlugin('chai');
suite('index', () => {
before(({ remote }) => {
return remote
.get('_dist/src/index.html')
.setFindTimeout(5000)
.findDisplayedByCssSelector('body.loaded');
});
test('greeting form', ({ remote }) => {
return remote
.findById('nameField')
.click()
.type('Elaine')
.end()
.findByCssSelector('#loginForm input[type=submit]')
.click()
.end()
.findById('greeting')
.getVisibleText()
.then(text => {
assert.strictEqual(
text,
'Hello, Elaine!',
'Greeting should be displayed when the form is submitted'
);
});
});
});
It could also be written using async/await:
const { suite, test, before } = intern.getPlugin('interface.tdd');
const { assert } = intern.getPlugin('chai');
suite('index', () => {
before(async ({ remote }) => {
await remote.get('_dist/src/index.html');
await remote.setFindTimeout(5000);
await remote.findDisplayedByCssSelector('body.loaded');
});
test('greeting form', async ({ remote }) => {
const name = await remote.findById('nameField');
await name.click();
await name.type('Elaine');
const button = await remote.findByCssSelector(
'#loginForm input[type=submit]'
);
await button.click();
const greeting = await remote.findById('greeting');
const text = await greeting.getVisibleText();
assert.strictEqual(
text,
'Hello, Elaine!',
'Greeting should be displayed when the form is submitted'
);
});
});
_Note: To learn which methods are available on the remote
object, check
Leadfoot’s
Command object documentation._
In the code above, calling remote.get
loads the HTML page we want to test into
the browser. Then, we wait for the “loaded” CSS class to appear on the body, for
a maximum of five seconds. Once this element exists, we go through the process
of finding, clicking, and typing into elements. Finally, we retrieve the text
from the greeting element and check it to confirm that it matches what was
expected.
Now that this test module is complete, the final step is to add it to our Intern
configuration in the special functionalSuites
top-level property:
"functionalSuites": "_dist/tests/functional/index.js",
Now if we go back and run the same npm test
command from the end of Steps 2
and 3, we will see our unit tests running in both Node.js and Chrome, our
functional tests running in Chrome, and all of them passing:
Listening on localhost:9000 (ws 9001)
Tunnel started
✓ node - hello - greet (0.001s)
No unit test coverage for node
node: 1 passed, 0 failed
‣ Created remote session chrome 59.0.3071.115 on MAC (f1bffddb-f4ae-46ba-8633-2357f925939d)
✓ chrome 59.0.3071.115 on MAC - hello - greet (0.001s)
✓ chrome 59.0.3071.115 on MAC - index - greeting form (0.236s)
No unit test coverage for chrome 59.0.3071.115 on MAC
chrome 59.0.3071.115 on MAC: 2 passed, 0 failed
TOTAL: tested 2 platforms, 3 passed, 0 failed
At this point, all of our unit and functional tests are passing. The next step
is enabling code coverage. Intern is unique in that it not only runs unit and
functional tests in one command, but it can also gather coverage information for
both types of tests as well! To enable code coverage, set the "coverage"
property of the top-level object in intern.json
to a glob pattern (or an array
of glob patterns) of compiled files to cover:
"coverage": [
"_dist/src/**/*.js",
"!_dist/src/system.config.js"
]
This will tell Intern to get coverage information for all JavaScript files in
_dist/src
except for _dist/src/system.config.js
. Now when we run npm test
,
the output will tell us the coverage we have:
Listening on localhost:9000 (ws 9001)
Tunnel started
✓ node - hello - greet (0.001s)
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
node: 1 passed, 0 failed
‣ Created remote session chrome 59.0.3071.115 on MAC (dbc5f5cc-2f89-43e1-b062-7df608334314)
✓ chrome 59.0.3071.115 on MAC - hello - greet (0.001s)
✓ chrome 59.0.3071.115 on MAC - index - greeting form (0.231s)
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
chrome 59.0.3071.115 on MAC: 2 passed, 0 failed
Total coverage
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
main.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
TOTAL: tested 2 platforms, 3 passed, 0 failed
One thing to note: Intern (via Istanbul) automatically remaps the coverage
information back to our source files using source maps. We can see that the unit
tests only report coverage for hello.ts
, but our functional test - since it
exercises the entire application - reports coverage for main.ts
as well.
Intern also allows us to output an HTML coverage report to see graphically which
lines have been exercised by our tests. To enable this feature, add the
htmlcoverage
reporter in the top-level object of intern.json
:
"reporters+": "htmlcoverage"
As you can see, instead of using "reporters"
, we have used "reporters+"
.
This will add "htmlcoverage"
to the default array of reporters instead of
overriding it. When we run npm test
we’ll still see the same output as before
in the console, and we will also have an HTML report in
intern-tutorial/coverage/
.
At this point, all our tests are written and running in Node.js and Chrome. The
only thing that’s left to do is to run all our tests on all the platforms we
want to support. We’ll do this by setting up a browserstack
configuration
within intern.json
to run our tests with BrowserStack:
"reporters+": "htmlcoverage",
"configs": {
"browserstack": {
"tunnel": "browserstack",
"maxConcurrency": 2,
"capabilities": {
"idle-timeout": 60,
"fixSessionCapabilities": "no-detect"
},
"environments": [
{ "browser": "internet explorer", "version": [ "10", "11" ] },
{ "browser": "firefox", "version": [ "latest" ], "platform": [ "WINDOWS", "MAC" ] },
{ "browser": "chrome", "version": [ "latest" ], "platform": [ "WINDOWS", "MAC" ] },
{ "browser": "safari", "version": [ "9", "10" ] }
]
}
}
This sets up a child configuration named browserstack
with our environments
and tunnel. Intern will use this information to communicate with our remote
testing service (in this case, BrowserStack) to run our unit and functional
tests in all of the browsers we specified in our environments
array, reporting
back test results and coverage for each browser. Since we are using
BrowserStack, we will need to provide our credentials and our child
configuration name:
BROWSERSTACK_USERNAME=<your username> BROWSERSTACK_ACCESS_KEY=<your access key> npm test config=@browserstack
You can also specify your username and access key on the tunnelOptions
object
in your Intern configuration, using the username
and apiKey
keys, if you
don’t want to put them on the command line:
"tunnel": "browserstack",
"tunnelOptions": {
"username": "<your username>",
"apiKey": "<your access key>"
},
"maxConcurrency": 2,
However, keep in mind that keeping this information in a configuration file can expose your username and access key to others if the file is checked into a public repository.
If everything was done correctly, you should see the results of the test run being output to your terminal:
Listening on localhost:9000 (ws 9001)
Tunnel started
✓ node - hello - greet (0.001s)
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
node: 1 passed, 0 failed
‣ Created remote session internet explorer 10 on WINDOWS (2ebde91d356d8dd326514caddc3e600a0aeb58f6)
✓ internet explorer 10 on WINDOWS - hello - greet (0s)
✓ internet explorer 10 on WINDOWS - index - greeting form (1.832s)
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
internet explorer 10 on WINDOWS: 2 passed, 0 failed
‣ Created remote session internet explorer 11 on WINDOWS (f8ab8d0a315172af0337b9fcd91ae3457098a906)
‣ Created remote session firefox on windows_nt 6.3 (1cb854da49290a1818d8147244c069504c51775b)
✓ internet explorer 11 on WINDOWS - hello - greet (0s)
✓ firefox on windows_nt 6.3 - hello - greet (0.001s)
✓ internet explorer 11 on WINDOWS - index - greeting form (3.618s)
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
...
Total coverage
----------|----------|----------|----------|----------|----------------|
File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines |
----------|----------|----------|----------|----------|----------------|
All files | 100 | 100 | 100 | 100 | |
hello.ts | 100 | 100 | 100 | 100 | |
main.ts | 100 | 100 | 100 | 100 | |
----------|----------|----------|----------|----------|----------------|
TOTAL: tested 9 platforms, 17 passed, 0 failed
When you start testing your actual application, it’s a good idea to use Intern in conjunction with a continuous integration service like Travis CI or Jenkins so you know that the code in your repository is passing its tests at all times, and so you can monitor your code coverage figures. Instructions are available in the continuous integration section of the documentation for running Intern with Jenkins, Travis CI, and TeamCity.
If you’d like a complete working copy of this project with Intern already configured and the tests already written, download the completed-tutorial branch. If you have any questions, please let us know. Pull requests to enhance this tutorial are also accepted and appreciated!
Once you’re ready to dive in and start writing tests for your own application, take a look at Intern’s project documentation. It contains references and documentation for all of the features of Intern.
Happy testing!