This tutorial is out of date and no longer maintained.
In this tutorial, we will be building and testing an employee directory for a fictional company. This directory will have a view to show all of our users along with another view to serve as a profile page for individual users. Within this part of the tutorial, we’ll focus on building the service and its tests that will be used for these two views.
The primary focus for this tutorial is testing so my assumption is that you’re comfortable working with JavaScript and AngularJS applications. As a result of this, I won’t be taking the time to explain what a factory is and how it’s used. Instead, I’ll provide you with code as we work through our tests.
For now, we’ll start with hard-coded data so we can get to writing tests ASAP. In future tutorials within this series, we’ll add an actual API to get as “real world” as possible.
From personal experience, tests are the best way to prevent software defects. I’ve been on many teams in the past where a small piece of code is updated and the developer manually opens their browser or Postman to verify that it still works. This approach (manual QA) is begging for a disaster.
Tests are the best way to prevent software defects.
As features and codebases grow, manual QA becomes more expensive, time-consuming, and error-prone. If a feature or function is removed does every developer remember all of its potential side-effects? Are all developers manually testing in the same way? Probably not.
The reason we test our code is to verify that it behaves as we expect it to. As a result of this process, you’ll find you have better feature documentation for yourself and other developers as well as a design aid for your APIs.
Karma is a direct product of the AngularJS team from struggling to test their own framework features with existing tools. As a result of this, they made Karma and rightly suggest it as their preferred test runner within the AngularJS documentation.
In addition to playing nicely with Angular, it also provides flexibility for you to tailor Karma to your workflow. This includes the option to test your code on various browsers and devices such as phones, tablets, and even a PS3 like the YouTube team.
Karma also provides you options to replace Jasmine with other testing frameworks such as Mocha and QUnit or integrate with various continuous integration services like Jenkins, TravisCI, or CircleCI.
Aside from the initial setup and configuration your typical interaction with Karma will be to run karma start
in a terminal window.
Jasmine is a behavior-driven development framework for testing JavaScript code that plays very well with Karma. Similar to Karma, it’s also the recommended testing framework within the AngularJS documentation. Jasmine is also dependency-free and doesn’t require a DOM.
As far as features go, I love that Jasmine has almost everything I need for testing built into it. The most notable example would be spies. A spy allows us to “spy” on a function and track attributes about it such as whether or not it was called, how many times it was called, and with which arguments it was called. With a framework like Mocha, spies are not built-in and would require pairing it with a separate library like Sinon.js.
The good news is that the switching costs between testing frameworks are relatively low with differences in syntax as small as Jasmine’s toEqual()
and Mocha’s to.equal()
.
Imagine you had an alien servant named Adder who follows you everywhere you go. Other than being a cute alien companion Adder can really only do one thing, add two numbers together.
To verify Adder’s ability to add two numbers we could generate a set of test cases to see if Adder provides us the correct answer. So we could provide Adder with two positive numbers (2, 4), a positive number and a zero (3, 0), a positive number and a negative number (5, -2), and so on.
Our tests aren’t concerned with how Adder arrives at the answer. We only care about the answer Adder provides us. In other words, we only care that Adder behaves as expected - we have no concern for Adder’s implementation.
Before we get started verify you have nvm installed with version 5 of Node.js. If you already have Node.js and nodemon installed, you can skip this step.
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.1/install.sh | bash
- nvm install 5.0
- nvm use 5.0
- npm install -g nodemon
From here we can create the project directory and initial files we’ll need to start testing our code. If you’d prefer to follow along the code within this tutorial can be found on Github.
- mkdir meet-irl && cd meet-irl
- touch server.js
- mkdir app && cd app
- touch index.html app.js app.css
At this point your project structure should look like this:
|-meet-irl
|-app
|-app.css
|-app.js
|-index.html
|-server.js
We’re going to use Express as our server which will, within this tutorial, serve up our Angular application. Back in the root directory of this project let’s create a package.json
for our project.
- npm init
I left default values for everything except the entry point which I set to the file we just created, server.js
.
From here we can install the packages we’ll need.
- npm install express body-parser morgan path --save
Now that our dependencies are installed we can add this code to our server.js
file.
var express = require('express'),
app = express(),
bodyParser = require('body-parser'),
morgan = require('morgan'),
path = require('path');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(function(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, content-type, Authorization');
next();
});
app.use(morgan('dev'));
app.use(express.static(__dirname + '/app'));
app.get('*', function(req, res) {
res.sendFile(path.join(__dirname + '/index.html'));
});
app.listen(8080);
console.log('meet-irl is running on 8080');
This sends our app to index.html
within our app
directory so let’s add some content to that file as well.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="description" content="">
<meta name="author" content="">
<base href="/">
<title>MeetIrl</title>
<!--bootstrap and our app css file-->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
<link rel="stylesheet" href="app.css">
<!--angular and ui-router-->
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/0.2.18/angular-ui-router.js"></script>
<!--our angular app-->
<script src="app.js"></script>
</head>
<body ng-app="meetIrl">
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand">MeetIrl</a>
</div>
</div>
</nav>
<div ui-view>Hello, world!</div>
</body>
</html>
Within this file, I’ve included CDN references to Bootstrap for styling, Angular, ui-router, and the app.js
and app.css
files we made earlier. Within the body, we declare our ng-app
so let’s add that to app.js
.
(function() {
'use strict';
angular.module('meetIrl', [
'ui.router'
])
.config(function($urlRouterProvider) {
$urlRouterProvider.otherwise("/");
});
})();
With that change to app.js
running nodemon server.js
should serve a barebones Angular application to localhost:8080
with a navbar and the usual message: “Hello, world!”.
Before we start building out the Angular components of our application, we first need to install and set up Karma which is what we’re going to use to run our tests.
Back in the root directory of the app, we’ll need to install a few more packages. First, we’ll grab Karma and all of its associated plugins.
- npm install karma karma-jasmine jasmine-core karma-chrome-launcher --save-dev
While we’re at it let’s grab the Karma CLI as well.
- npm install -g karma-cli
Finally, we’ll install Angular, ui-router, and angular-mocks since we’ll have to provide these as dependencies to Karma. This will be explained in more detail shortly.
- npm install angular angular-ui-router angular-mocks --save-dev
At this point, we’re ready to create our Karma configuration file so that we can start writing tests. If you’ve installed the Karma CLI you can run karma init
.
Similar to npm init
you’ll see a series of questions that help you setup your configuration file. The questions are listed below along with the options you should provide. If there’s nothing to the right of an *
just hit your return
key.
Once you’re done you should see a message alerting you of your newly created file. Open the file and you should see something like this. (Note: Comments removed for brevity)
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
],
exclude: [
],
preprocessors: {
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
concurrency: Infinity
})
}
Our first test is going to test the service we’re going to use for managing our users. For now, we’re going to have one service method that returns a hard-coded list of users to mock an actual API.
Eventually, we will integrate an actual API into our service but for now, we’re going to keep things as simple as possible. This style of development also mimics the real world if you’re working with backend developers who may be building their part of an application at the same time as the frontend.
First, let’s create a new directory for our services along with another directory specifically for the Users
service and its associated test file.
- cd app
- mkdir services && cd services
- mkdir users && cd users
- touch users.js users.spec.js
With these additions your project structure should now look like this:
|-meet-irl
|-app
|-services
|-users
|-users.js
|-users.spec.js
|-app.css
|-app.js
|-index.html
|-server.js
The users.js
file will be used for our service and the users.spec.js
file will be used to test the service. Open users.spec.js
and we can start writing our first test.
When we’re using Jasmine to test our code we group our tests together with what Jasmine calls a “test suite”. We begin our test suite by calling Jasmine’s global describe
function. This function takes two parameters: a string and a function. The string serves as a title and the function is the code that implements our tests.
We can set up our first test suite by adding the following code to users.spec.js
.
describe('Users factory', function() {
});
Within this describe block we’ll add specs. Specs look similar to suites since they take a string and a function as arguments but rather than call describe
we’re going to call it
instead. Within our it
block is where we put our expectations that tests our code.
An example spec added to our describe block would look like this:
// Be descriptive with titles here. The describe and it titles combined read like a sentence.
describe('Users factory', function() {
it('has a dummy spec to test 2 + 2', function() {
// An intentionally failing test. No code within expect() will never equal 4.
expect().toEqual(4);
});
});
Similar to our describe
block, we’ve provided a brief summary of our test in the form of a string. In this case, we’re doing a sample test, basic addition, to illustrate the structure of a test.
Within the function, we call expect
and provide it what is referred to as an “actual”. We’ve left it empty for now but we’ll update it momentarily. After the expect
is the chained “Matcher” function, toEqual()
, which the testing developer provides with the expected output of the code being tested.
This is enough to do the “Hello, world” equivalent of testing so let’s do that. But before we run Karma, we’ll have to add this file to our Karma configuration file. So open karma.conf.js
and update the files
property with our new test file.
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['jasmine'],
files: [
'./app/services/users/users.spec.js'
],
exclude: [
],
...
})
}
Save the file, run karma start
, and you should see Karma display an error message for your test.
In our karma.conf.js
file we have singleRun
set to false
so Karma should automatically restart any time you save changes to your tests or code. Leave this running in a separate terminal window and you won’t have to run karma start
every time we write or update a test.
Since we didn’t pass anything into expect()
it evaluated to undefined
. Since undefined
isn’t equal to 4
our test failed. So far so good. Now let’s make it pass with this small bit of code.
// Adding code to expect()
expect(2 + 2).toEqual(4);
Run karma start
again if it’s not already running and you should now see a success message. Well done Javascript addition operator.
We wrote a failing test and made it pass but that was basic JavaScript. We’re here to test Angular code so let’s do it. But first, let’s think about the smallest bit of a working factory we could test.
A factory is typically a JavaScript object with properties that evaluate to function expressions. The simplest factory we could possibly write would be a factory that returns an empty object. In other words, it’s a defined factory with no available methods.
First, we’ll write the test for this behavior and then we’ll write the factory to make the test pass.
As of right now your users.spec.js
file should look like this:
describe('Users factory', function() {
it('has a dummy spec to test 2 + 2', function() {
expect(2 + 2).toEqual(4);
});
});
Remove the 3-line it
block and update it to this:
describe('Users factory', function() {
var Users;
// Before each test load our api.users module
beforeEach(angular.mock.module('api.users'));
// Before each test set our injected Users factory (_Users_) to our local Users variable
beforeEach(inject(function(_Users_) {
Users = _Users_;
}));
// A simple test to verify the Users factory exists
it('should exist', function() {
expect(Users).toBeDefined();
});
});
There’s some new, weird-looking code here so let’s break this down.
First I’ve created a variable Users
which will be set to the factory we’re eventually going to write.
Then there’s beforeEach(angular.mock.module('api.users'));
. beforeEach()
is a function provided by Jasmine that allows us to run code before each test we’ve written is executed. angular.mock.module()
is a function provided to us by angular-mocks
which we installed earlier. We’ve specified our module here, api.users
, so that it’s available for us to use in our tests.
After that is another beforeEach
block with inject
being used to access the service we want to test - Users
.
So in plain English we’re saying, “Before each of my tests 1) load the module api.users
and 2) inject the Users
service (wrapped with underscores) and set it to the Users
variable I defined locally.”
The syntax here with our service name wrapped in underscores, _Users_
, is a convenient yet optional trick that’s commonly used so we can assign our service to the local Users
variable we created earlier. You could just write inject(function(Users)
and assign it to the local Users
variable but I think the surrounding underscores help to distinguish between the two and it’s more in line with conventions within the Angular community.
Finally, there’s a test spec that expects our Users
service to be defined. We’ll write the actual service methods once this initial test is passing!
If you save this file you should see an error message stating angular is not defined
.
The reason this is happening is because Karma only has access to one file - users.spec.js
. That was fine earlier when we were testing basic JavaScript like 2 + 2
but now we’re testing Angular and Karma needs Angular to test it. So open up karma.conf.js
and update the files
property from this:
frameworks: ['jasmine'],
files: [
'./app/services/users/users.spec.js'
],
exclude: [],
To this:
frameworks: ['jasmine'],
files: [
'./node_modules/angular/angular.js', // angular
'./node_modules/angular-ui-router/release/angular-ui-router.js', // ui-router
'./node_modules/angular-mocks/angular-mocks.js', // loads our modules for tests
'./app/services/users/users.js', // our Users factory
'./app/app.js', // our angular app
'./app/services/users/users.spec.js' // our test file for our Users factory
],
exclude: [],
In addition to our test file, we’re now including angular.js
and its other dependency in our application angular-ui-router
. We’ve also included angular-mocks
, the module that provides us the ability to load in our Angular modules to test. Finally, we’ve added our app.js
which initializes our angular app.
Save this, run karma start
again, and now you should see an error stating module api.users
cannot be found. All that’s left now is to create that module and its respective service.
The order of our files within the files
property is important since it determines the order in which they’re loaded into the browser when Karma is run. That’s why Angular and all of its related code are placed up top, then the application files, and finally the test files.
Back in app/services/users
open users.js
and add the following code to create our Users
factory which returns an empty object.
(function() {
'use strict';
// Creating the module and factory we referenced in the beforeEach blocks in our test file
angular.module('api.users', [])
.factory('Users', function() {
var Users = {};
// Users.method = function() {};
return Users;
});
})();
Save that file and you should now see a passing test!
Now that we know our test is successfully testing the existence of our Users
service let’s add an all
method that will return a collection of users. This method will eventually be used to display the list of users shown in the image at the start of this tutorial.
Continuing with the progressive additions to our code, jump back into users.spec.js
and add a new suite and spec for our new service method.
describe('Users factory', function() {
var Users;
// Load our api.users module
beforeEach(angular.mock.module('api.users'));
// Set our injected Users factory (_Users_) to our local Users variable
beforeEach(inject(function(_Users_) {
Users = _Users_;
}));
// A simple test to verify the Users service exists
it('should exist', function() {
expect(Users).toBeDefined();
});
// A set of tests for our Users.all() method
describe('.all()', function() {
// A simple test to verify the method all exists
it('should exist', function() {
expect(Users.all).toBeDefined();
});
});
});
Save this and you should see a failing test for the Users.all
method because it doesn’t exist yet. Jump back into users.js
and add the new method.
(function() {
'use strict';
// Creating the module and factory we referenced in the beforeEach blocks in our test file
angular.module('api.users', [])
.factory('Users', function() {
var Users = {};
// Defining all to make our test pass. It doesn't need to do anything yet.
Users.all = function() {
};
return Users;
});
})();
Save that and our previously failing test should now be passing. The test says nothing about what all
does, it only expects that the method is defined. We want it to return a list of users we can use to populate a view in our app, so let’s add another spec to handle that.
First, we’ll need to add a local variable for our hard-coded set of users.
describe('Users factory', function() {
var Users;
// The array of users our factory will provide us
var userList = [
{
id: '1',
name: 'Jane',
role: 'Designer',
location: 'New York',
twitter: 'example_jane'
},
{
id: '2',
name: 'Bob',
role: 'Developer',
location: 'New York',
twitter: 'example_bob'
},
{
id: '3',
name: 'Jim',
role: 'Developer',
location: 'Chicago',
twitter: 'example_jim'
},
{
id: '4',
name: 'Bill',
role: 'Designer',
location: 'LA',
twitter: 'example_bill'
}
];
...
Then we’ll add another spec to test the expected behavior of our method right below our previous test.
...
// A set of tests for our Users.all() method
describe('.all()', function() {
// A simple test to verify the method all exists
it('should exist', function() {
expect(Users.all).toBeDefined();
});
// A test to verify that calling all() returns the array of users we hard-coded above
it('should return a hard-coded list of users', function() {
expect(Users.all()).toEqual(userList);
});
});
});
Now that this test has been written we should have one failing test. The test for our Users
factory is expecting a list of users when we call .all()
but our service doesn’t return anything. So update users.js
to return the same array of users …
...
var Users = {};
var userList = [
{
id: '1',
name: 'Jane',
role: 'Designer',
location: 'New York',
twitter: 'example_jane'
},
{
id: '2',
name: 'Bob',
role: 'Developer',
location: 'New York',
twitter: 'example_bob'
},
{
id: '3',
name: 'Jim',
role: 'Developer',
location: 'Chicago',
twitter: 'example_jim'
},
{
id: '4',
name: 'Bill',
role: 'Designer',
location: 'LA',
twitter: 'example_bill'
}
];
Users.all = function() {
// Returning the array of users. Eventually this will be an API call.
return userList;
};
return Users;
...
Save the file and now the test should pass!
It may seem a little weird that our test was failing and we simply copied the user array from the test into our service but that’s a reflection of our test’s expecations. It doesn’t expect “real” data or data which must be served from an API. It simply states that when User.all()
is called we expect its return value to equal the hard-coded value we’ve supplied.
Assume our four users above are actually the founding members of a company. The requirement is that all four members should be displayed on the “Founders” section of their website.
If a developer added another user to userList
in users.js
the test would no longer pass. Our test expects the four founding members we provided via hard-coding in users.spec.js
. This addition in our service alters the expected behavior we specified in our test cases so the test fails as it should.
At this point the expectations specified in the tests would signal one of two things to the developer:
This is why developers often refer to test cases and their expectations for how our code should behave as documentation, or a design aid, for their actual code.
Now that we have a service method to return a list of all our users let’s add one extra service method to find a specific employee by their id
.
First, we’ll add a new test for our new service method. Like the previous service method we wrote, we’ll progressively build towards a completed feature. In users.spec.js
add a new describe
block below the one we created for Users.all
.
// A set of tests for our Users.all() method
describe('.all()', function() {
...
});
// A set of tests for our Users.findById() method
describe('.findById()', function() {
// A simple test to verify the method findById exists
it('should exist', function() {
expect(Users.findById).toBeDefined();
});
});
We can make this pass rather easily as we did before. In users.js
add the new service method.
Users.all = function() {
return userList;
};
// Defining findById to make our test pass. Once again, it doesn't need to do anything yet.
Users.findById = function(id) {
};
return Users;
Then we’ll add another spec where we actually call our method and provide an expectation.
describe('.findById(id)', function() {
// A simple test to verify the method findById exists
it('should exist', function() {
expect(Users.findById).toBeDefined();
});
// A test to verify that calling findById() with an id, in this case '2', returns a single user
it('should return one user object if it exists', function() {
expect(Users.findById('2')).toEqual(singleUser);
});
});
Notice we’ve referenced singleUser
in the matcher function. In this case that would be the user with id: '2'
. So just below the previous variable we hard-coded, userList
, add another variable for our single user object.
describe('Users factory', function() {
var Users;
//The array of users our factory will provide us
var userList = [
...
];
//The single user we expect to receive when calling findById('2')
var singleUser = {
id: '2',
name: 'Bob',
role: 'Developer',
location: 'New York',
twitter: 'example_bob'
};
We have yet to implement findById
so our test is currently failing. Add this piece of code, or your preferred implementation that returns a single object, and the test should now pass.
Users.findById = function(id) {
// Returning a single user object as our test expects it to
return userList.find(function(user) {
return user.id === id;
});
};
return Users;
There’s only one thing left to do now to wrap up the service and tests we’ll need to implement the rest of our employee directory. Eventually, when we create the profile page for each employee we’re going to find a user based on the id
within the URL. We’ve already verified our method can return a user if it exists but in the case of a user that cannot be found, we’ll redirect them to a 404 page with a link back to our user directory.
So for the purpose of our findById
method, we expect a user object to be returned if it exists. Otherwise, it’ll evaluate to undefined
and our controller, which we’ll build in the next part of this tutorial, will redirect the user to our to-be-created 404 page.
With that in mind open up users.spec.js
one more time and add this test to the describe
block for findById()
.
describe('.findById(id)', function() {
// A simple test to verify the method findById exists
it('should exist', function() {
expect(Users.findById).toBeDefined();
});
// A test to verify that calling findById() with an id, in this case '2', returns a single user
it('should return one user object if it exists', function() {
expect(Users.findById('2')).toEqual(singleUser);
});
// A test to verify that calling findById() with an id that doesn't exist, in this case 'ABC', returns undefined
it('should return undefined if the user cannot be found', function() {
expect(Users.findById('ABC')).not.toBeDefined();
});
});
This test should automatically pass as a side effect of using Array.find()
which you can read about here.
At this point, I hope testing, both its benefits and the reason for writing them, is starting to become clear. Personally, I pushed off testing for the longest time and my reasons were primarily because I didn’t understand the why behind them, and resources for setting it up were limited.
What we’ve created in this tutorial isn’t visually impressive but it’s a step in the right direction. Now that a service is working and tested, all we have to do now is add our controllers and their associated tests and then we can populate our views to mimic the screenshot I posted at the start.
If you’d like your tests to display in your terminal window in a more readable fashion install this npm package.
- npm install karma-spec-reporter --save-dev
Then in your karma.conf.js
change the value for reporters
from progress
to spec
.
module.exports = function(config) {
config.set({
...
exclude: [
],
preprocessors: [
],
reporters: ['spec'],
...
})
}
With that change, the output from Karma should be much easier to digest.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!