Before you jump into the intricacies of testing an Angular 2 application, it's important to first examine the supporting infrastructure that will make running these tests possible. The bulk of official Angular resources offer tests on top of Karma and Jasmine, and there's no reason to rock the boat on this one, as these are both fine testing tools. That said, it's a whole new world with TypeScript involved, and using them in tests will require some considerations.
This recipe will demonstrate how to put together a very simple unit test suite. It will use Karma and Jasmine as the test infrastructure, TypeScript and Webpack for compilation and module support, and PhantomJS as the test browser. For those unfamiliar with these tools, here's a bit about them:
http://localhost:9876
in the desired test browser.The code, links, and a live example related to this recipe are available at .
Start out with a package.json
file:
[package.json] {}
This still needs to be a valid JSON file, as npm
needs to be able to parse it and add to it.
Start off by creating the file that will be tested. You intend to use TypeScript, so go ahead and use its syntax here:
[src/article.ts] export class Article { title:string = "Lab Mice Strike for Improved Working Conditions, Benefits" }
With the Article class defined, you can now import it into a new test file, article.spec.ts
, and use it.
Jasmine test files, by convention, are suffixed with .spec.ts
. Test files generated by the Angular CLI will exist alongside the file they test, but by no means is this mandatory. You can define your convention inside your Karma configuration later on.
Start off by importing the Article
class and create an empty Jasmine test suite using describe
:
[src/article.spec.ts] import {Article} from './article'; describe('Article unit tests', () => { });
describe
defines a spec suite, which includes a string title called Article unit tests, and an anonymous function, which contains the suite. A spec suite can be nested inside another spec suite.
Inside a describe
suite function, you can define beforeEach
and afterEach
, which are functions that execute before and after each unit test is defined inside the suite. Therefore, it is possible to define nested setup logic for unit tests using nested describe
blocks.
Inside the spec suite function, write the unit test that is using it:
[src/article.spec.ts] import {Article} from './article'; describe('Article unit tests', () => { it('Has correct title', () => { let a = new Article(); expect(a.title) .toBe("Lab Mice Strike for Improved Working Conditions, Benefits"); }); });
Note that both the code and the test are written in TypeScript.
First, install Karma, the Karma CLI, Jasmine, and the Karma Jasmine plugin:
npm install karma jasmine-core karma-jasmine --save-dev npm install karma-cli -g
Alternately, if you want to save a few keystrokes, the following is equivalent:
npm i -D karma jasmine-core karma-jasmine npm i karma-cli -g
Karma reads its configuration out of a karma.conf.js
file, so create that now:
[karma.conf.js] module.exports = function(config) { config.set({ }) }
Karma needs to know how to find the test files and also how to use Jasmine:
[karma.conf.js] module.exports = function(config) { config.set({ frameworks: [ 'jasmine' ], files: [ 'src/*.spec.js' ], plugins : [ 'karma-jasmine', ] }) }
PhantomJS allows you to direct tests entirely from the command line, but Karma needs to understand how to use PhantomJS. Install the PhantomJS plugin:
npm install karma-phantomjs-launcher --save-dev
Next, incorporate this plugin into the Karma config:
[karma.conf.js] module.exports = function(config) { config.set({ browsers: [ 'PhantomJS' ], frameworks: [ 'jasmine' ], files: [ 'src/*.spec.js' ], plugins : [ 'karma-jasmine', 'karma-phantomjs-launcher' ] }) }
Karma now knows it has to run the tests in PhantomJS.
If you're paying attention closely, you'll note that the Karma config is referencing test files that do not exist. Since you're using TypeScript, you must create these files. Install TypeScript and the Jasmine type definitions:
npm install typescript @types/jasmine --save-dev
Add script definitions to your package.json
:
[package.json] { "scripts": { "tsc": "tsc", "tsc:w": "tsc -w" }, "devDependencies": { "@types/jasmine": "^2.5.35", "jasmine-core": "^2.5.2", "karma": "^1.3.0", "karma-cli": "^1.0.1", "karma-jasmine": "^1.0.2", "karma-phantomjs-launcher": "^1.0.2", "typescript": "^2.0.3" } }
Create a tsconfig.json
file. Since you're fine with the compiled files residing in the same directory, a simple one will do:
[tsconfig.json] { "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "node" } }
You would probably not do it this way for a production application, but for a minimum viable setup, this will do in a pinch. A production application would most likely put compiled files into an entirely different directory, frequently named dist/
.
Of course, you'll need a way of resolving module definitions for code and tests. Karma isn't capable of doing this on its own, so you'll need something to do this. Webpack is perfectly suitable for such a task, and Karma has a terrific plugin that allows you to preprocess your test files before they reach the browser.
Install Webpack and its Karma plugin:
npm install webpack karma-webpack --save-dev
Modify the Karma config to specify Webpack as the preprocessor. This allows your module definitions to be resolved properly:
[karma.conf.js] module.exports = function(config) { config.set({ browsers: [ 'PhantomJS' ], frameworks: [ 'jasmine' ], files: [ 'src/*.spec.js' ], plugins : [ 'karma-webpack', 'karma-jasmine', 'karma-phantomjs-launcher' ], preprocessors: { 'src/*.spec.js': ['webpack'] } }) }
You can kick off the Karma server with the following:
karma start karma.conf.js
This will initialize the test server and run the tests, watching for changes and rerunning the tests. However, this sidesteps the fact that the TypeScript files require compilation in the files that Karma is watching. The TypeScript compiler also has a file watcher that will recompile on the fly. You would like both of these to recompile whenever you save changes to a source code file, so it makes sense to run them simultaneously. The concurrently
package is suitable for this task.
concurrently
not only allows you to run multiple commands at once, but also to kill them all at once. Without it, a kill signal from the command line would only target whichever process was run most recently, ignoring the process that is running in the background.
Install concurrently
with the following:
npm install concurrently --save-dev
Finally, build your test script to run Karma and the TypeScript compiler simultaneously:
[package.json] { "scripts": { "test": "concurrently 'npm run tsc:w' 'karma start karma.conf.js'", "tsc": "tsc", "tsc:w": "tsc -w" }, "devDependencies": { "@types/jasmine": "^2.5.35", "concurrently": "^3.1.0", "jasmine-core": "^2.5.2", "karma": "^1.3.0", "karma-cli": "^1.0.1", "karma-jasmine": "^1.0.2", "karma-phantomjs-launcher": "^1.0.2", "karma-webpack": "^1.8.0", "typescript": "^2.0.3", "webpack": "^1.13.2" } }
With this, you should be able to run your tests:
npm test
If everything is done correctly, the Karma server should boot up and run the tests, outputting the following:
PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 SUCCESS (0.038 secs / 0.001 secs)
Karma and Jasmine work together to deliver test files to the test browser. TypeScript and Webpack are tasked with converting your TypeScript files into a JavaScript format that will be usable by the test browser.
An interesting consideration of this setup is how exactly TypeScript is handled.
Both the code and test files are written in TypeScript, which allows you to use the ES6 module notation, as opposed to some mix-and-match strategy. However, this leaves you with some choices to make on how the test setup should work.
The tests need to be able to use different pieces of your application in a piecemeal fashion, as opposed to the standard application setup where all the modules get pulled together at once. This recipe had TypeScript independently compile the .ts
files, and it then directed Karma to watch the resultant .js
files. This is perhaps easier to comprehend by someone who is easing into tests, but it might not be the most efficient way to go about it. Karma also supports TypeScript plugins, which allow you to preprocess the files into TypeScript before handing them off to the Webpack preprocessor.
Karma supports the chaining of preprocess steps, which will be useful if you want to compile the TypeScript on the fly as part of preprocessing.