Software in context

Tags


Testing AngularJS with Grunt, Karma, and Jasmine

27th September 2014

Bikram Yoga Pose

A programmer friend of mine lives in the pure realm of Java, where everything is a kind of thing, and coding is more like shepherding Eclipse than it is like typing. We often engage in lively discussions about the pros and cons of statically- vs. dynamically-typed languages. I'm not a doctrinaire about either one, although I currently write mostly JavaScript, Python, and Erlang, which means I can better argue for the latter. After many years, both classes of languages continue to exist and flourish in different ecosystems, just like diverse biologies in nature. A masterful programmer will incorporate the benefits of both, no matter the language at hand.

A primary benefit of static typing is early warning about when something won't work, at compile-time. JavaScript has no such compiler, and code quality tools like strict mode and JsHint can only get you so far before you have to run your code and see what happens. Apps written in languages like these need something more to achieve stability at scale. Automated testing meets this need. What I've taken from the discussions with my friend is the necessity of having a great suite of unit tests for your application, especially those written with dynamically typed languages. AngularJS's testing guideline puts is well:

JavaScript is a dynamically typed language which comes with great power of expression, but it also comes with almost no help from the compiler. For this reason we feel very strongly that any code written in JavaScript needs to come with a strong set of tests.

AngularJS

Unit testing lets you codify the way the pieces of an application should behave, which is more than can be said of a static type checker, as helpful as it may be. This is a superior kind of early-warning. But testing isn't just about making sure the code does the stuff I want it to do. A well-designed unit test suite forces your code to be testable, and therefore more functional, readable, and modular. Good unit testing is code sharpening code. It's like Bikram Yoga, where 26 poses systematically stretch and strengthen the body at the same time (at 104°).

In this post I'll share the unit testing setup I've settled on for AngularJS apps. We won't cover end-to-end or browser simulation tests, although the tools presented below are more than capable of such testing. General knowledge of NodeJS-based projects is assumed. Here are the key players:

  1. AngularJS — An opinionated JavaScript framework for creating Single Page Apps. Angular relies heavily on dependency injection, which greatly simplifies isolating and testing components. It also provides some handy utilities for mocking out objects.
  2. Grunt — A taskrunner for NodeJS-based projects. Grunt can help automate building a project. It's pluggable architecture supports tasks for testing, concatenating, and minifying JavaScript code, among many more.
  3. Jasmine — A test framework for JavaScript. Jasmine provides the functions needed to write tests against our application. Simple, readable, and the popular choice for testing Angular projects on the web.
  4. Karma — A test runner for JavaScript that can be integrated into a build process. Karma programmatically finds and runs tests in real web browsers, and supports various test frameworks, including Jasmine.

Approach. Given a NodeJS/Angular project, we'll develop a new Grunt task that can be run with the command grunt test. This task will cause Karma to find and run our Jasmine tests, with the help of the ngMock module.

1. Add Testing Tools to Project

Since we're working with a node-based project, we have npm, the node package manager at our disposal. Run the following to install (and remember) the testing tools:

npm install karma --save-dev  
npm install karma-jasmine --save-dev  
npm install karma-phantomjs-launcher --save-dev  

This will install Karma and the plugins we'll need to run Jasmine tests. These are explained in more detail below. Angular's ngMock module will provide some stubs and utilities for unit testing. We can install it with bower, a front-end package manager analogous to npm in the back-end:

bower install angular-mocks --save-dev  

The --save-dev flag will persist these plugins to the devDependencies sections of package.json and bower.json, respectively. Now we can be assured that running npm install and bower install will set up our latest environment.

2. Update the Gruntfile

The Gruntfile is a JavaScript file where all of the Grunt tasks are specified. We'll create a new task runnable from the command line via grunt test.

2.1 Load the grunt-karma plugin

First, let grunt know about the grunt-karma plugin we installed to the project.

grunt.loadNpmTasks('grunt-karma');  

I like to keep all my plugin loading in a group just before the main grunt.initConfig() function.

2.2 Add a karma block

Add a task block to your grunt.initConfig() function. This is where the Karma runner learns the specifics of our project:

karma: {  
  unit: {
    options: {
      frameworks: ['jasmine'],
      singleRun: true,
      browsers: ['PhantomJS'],
      files: [
        'public/components/angular/angular.js',
        'public/components/angular-mocks/angular-mocks.js',
        'src/js/**/*.js'
      ]
    }
  }
}

Let's break this down.

1. Define the unit target.

karma: {  
  unit: {
    options: {

Karma also supports other targets for different testing modes, like dev or continuous. We're developing a target appropriate for single-shot unit testing that can easily be incorporated into a build process. The goal is to have a single command that will run our entire unit test suite, outputting success or failure.

2. Use the Jasmine test framework.

frameworks: ['jasmine']  

We've already installed the karma-jasmine dependency, so we're good to go. Karma also supports other frameworks like qunit, mocha, or nodeunit.

3. Enable singleRun.

singleRun: true  

This will shut down the Karma server when testing is complete, which is appropriate for our single-shot requirement. Disabling singleRun would be better for a continuous testing mode.

4. Use the PhantomJS browser.

browsers: ['PhantomJS']  

A primary strength of Karma is that there are plugins that let you run tests in a variety of real browsers. PhantomJS is a headless browser perfect for quickly running unit tests from the command line. Our code will basically think its running in a real web browser even though we can't see one.

5. Specify a set of JavaScript files to load for testing.

files: [  
    'public/components/angular/angular.js',
    'public/components/angular-mocks/angular-mocks.js',
    'src/js/**/*.js'
]

Every JavaScript file listed in the files array will be loaded (executed) by Karma into our PhantomJS scenario in the order listed, just as if they had been added as <script> elements into a real web page. Any Jasmine tests that run incidentally will be recognized by Karma. I make sure to load (1) angular and it's dependencies, (2) angular mocks, and then (3) all my app's source code. Note that the first three entries are loaded from public/components/ because they are third-party components installed there by bower.

My Jasmine tests live in separate files scattered throughout the src folder. By convention these are named like *.tests.js after the module or component they are testing. For example, foobarService.js will have a corresponding test file called foobarService.tests.js. Again, Karma doesn't care what the tests are named, as long as they're included somehow in the files list and executed when loaded. The Jasmine dependency itself is automatically included due to our declared use of the Jasmine framework.

2.3 Exclude tests from production code

Before we see what a Jasmine test looks like, let's make sure we don't package the test files up with our app's production code. This is simple enough given that the test files are distinctly named. The most sensible place for me to do the exlusion is within the concat task:

concat: {  
  options: {},
  dist: {
    files: {
      'public/js/app.js': [
         'src/js/**/*.js',
         '!src/js/**/*.tests.js'
       ]
    }
  }
},

The line '!src/js/**/*.tests.js' uses the ! symbol to exclude all test files before handing the concatenated result off to be uglified.

2.4 Register a test task

The final Gruntfile modification is to add a block like the following:

grunt.registerTask('test', [  
  'jshint',
  'karma'
]);

My convention is to register tasks at the bottom, after the initConfig() function. Since we already defined a karma task, we could just run grunt karma from the command line to exercise unit tests. But we'll preclude perplexing test failures by always running jshint to catch syntax errors before we run tests.

3. Write a Test

Now we're at liberty to write Jasmine tests and put them anywhere in the /src/js/. Karma will pick them up and run them automatically when we run grunt test, and as long as they're named like *.tests.js, they won't be included in the concatenated+uglified production code.

Here's a simple example of a Jasmine unit test against an angular service. Say we have a service that outputs a Fibonacci number:

fibonacciService.js

angular.module('ExampleApp', [])

.service('FibonacciService', [function() {

  // iterative approach
  this.fibonacci = function(num) {
    var prev1 = 1,
        prev2 = 0,
        current = 0;
    for (var n = 2; n <= num; n++) {
      current = prev1 + prev2;
      prev2 = prev1;
      prev1 = current;
    }
    return current;
  };

}]);

Then a matching Jasmine test could look like the following.

fibonacciService.tests.js

describe('ExampleApp.FibonacciService', function() {

  var FibonacciService;

  beforeEach(module('ExampleApp'));
  beforeEach(inject(function($injector) {
    FibonacciService = $injector.get('FibonacciService');
  }));

  it('Should output correct Fibanacci numbers', function() {
    expect(FibonacciService.fibonacci(0)).toBe(0);
    expect(FibonacciService.fibonacci(1)).toBe(1);
    expect(FibonacciService.fibonacci(10)).toBe(55);
  });

});

Let's step through this:

1. Declare a Jasmine suite

describe('ExampleApp.FibonacciService', function() {  

Jasmine calls itself a "behavior-driven development framework for testing JavaScript code". Aside from testing the behavior of code, this probably refers to the way it wants you to organize your tests within describe and it functions. These container functions are meant to match up somehow with (1) units of code and (2) behaviors of that unit, respectively. The top-level describe function defines a test suite. In this case, our service gets its own suite.

2. Declare a var for the service under test.

var FibonacciService;  

We declare a suite-level variable that will get the service in question assigned to it.

3. Inject a mock module before each test.

beforeEach(module('ExampleApp'));  

Here's where the ngMock module becomes into play. Before each test, we can provide a light-weight version of our ExampleApp module better suited for unit testing.

4. Inject the service in question before each test.

beforeEach(inject(function($injector) {  
  FibonacciService = $injector.get('FibonacciService');
}));

Before each test we also directly inject the service in question. We could do this a single time within the describe function, but its a better practice to inject a fresh instance of the service before each it block, in case there's any state that gets built up under the service's hood that we'd to flush on a per-spec basis.

The injection utilities provided by ngMock become very powerful very quickly. For example, if our service had its own dependencies, we could use the $injector to provide these to the test context without having to jump through hoops to instantiate a testable version of the service. And this is all enabled by Angular's decision to use dependency injection.

5. Declare a Jasmine spec.

it('Should output correct Fibanacci numbers', function() {  

With Jasmine, describe is to suite as it is to spec. A spec is just a class of behaviors within a suite. Use specs to test one aspect of the behavior of a unit of code. These building blocks can be used to compose any testing structure; suites can contain many specs, and even sub-suites. Similarly, specs can contain many expectations

6. Expect behavior from your app code.

expect(FibonacciService.fibonacci(0)).toBe(0);  
expect(FibonacciService.fibonacci(1)).toBe(1);  
expect(FibonacciService.fibonacci(10)).toBe(55);  

The expect function defines… an expectation! Expectations get a readably-named matcher like toBe or toContain chained to them. There are a variety of matchers ot choose from. I'm not sure what would happen if we ran fibonacci(-1) or fibonacci('lizard'), but Jasmine could sure help set our expectations.

4. Run the Tests!

Now you should be able to run your entire unit test collection from the command line. If your tests pass, you should see output like:

$ grunt test

Running "jshint:files" (jshint) task  
>> 2 files lint free.

Running "karma:unit" (karma) task  
INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/  
INFO [launcher]: Starting browser PhantomJS  
INFO [PhantomJS 1.9.7 (Mac OS X)]: Connected on socket wF3uEXTeFWqciwTPR5nw with id 26647153  
PhantomJS 1.9.7 (Mac OS X): Executed 3 of 3 SUCCESS (0.006 secs / 0.027 secs)  

Thanks! The setup is a bit heavy, but totally worth it. Remember, testing isn't about buying peace of mind, it's about writing clearer, more predicatable , more portable application code. Don't let your code stand naked. Get some coverage!

Caleb Sotelo
AUTHOR

Caleb Sotelo

I'm a Software Engineer and Director of OpenX Labs. I try to write about software in a way that helps people truly understand it. Follow me @calebds.

View Comments