Testing AngularJS with Jasmine and Karma (Part 1)

Our Goal

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.

What You Should Know

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.

Why Test?

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.

Why Karma?

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.

Why Jasmine

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 is relatively low with differences in syntax as small as Jasmine's toEqual() and Mocha's to.equal().

A Simple Test Example

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.

AngularJS Applicaton Setup

Before we get started verify you have nvm installed with version 5 of node. If you already have node and nodemon installed, you can skip this step.

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.

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)

Our First Test

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 mimicks 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.

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 setup 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 expecations that tests our code.

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" equivelent 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.

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 everytime 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.

Testing an Angular Factory

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.

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:

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.

Adding Users to the Users Service

Now that we know our test is successully 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.

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.

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 ...

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:

Their code change was in the wrong section of the app. This is only for founding members so perhaps there's an employee section with an employee array somewhere else in the codebase.

A new founding member was added to the team. This would essentially be a change in requirements. At this point the user service and test would both change to reflect the new requirement and our new expected behavior of the service.

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.

Finding a Single User

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.

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.

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.

Conclusion

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.

Extra

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.