Testing JavaScript with Jasmine

Testing JavaScript with Jasmine

Before we can begin testing our Angular applications, we need to gain an understanding of how testing JavaScript works in general.

Testing options

There are 3 very popular frameworks you can use for testing javascript: Jasmine, Mocha, and QUnit. They all essentially do the same thing, and even look almost identical in their syntax, so for our purposes we will use Jasmine for testing frontend JavaScript because it comes with everything we need already built-in.

How Jasmine works

Jasmine can be used as a standalone application where you download the framework and put your JavaScript files inside the src folder. Then you write the test files (often called "specs") and put them in the spec folder. The standalone Jasmine framework comes with a file called SpecRunner.html, which is simply the HTML file that puts all your source and spec files together, runs them in a browser using that browser's JavaScript engine, and returns a page that shows you how many/which of your tests passed and failed.

Since this is easier to show than explain, let's walk through a bare-bones example.


JavaScript Calculator

Download the standalone Jasmine files

Head to the Jasmine releases page to download the jasmine-standalone zip file. After it finished downloading, unzip the file and move it to a logical place in your computer. Mine will be in my ~/dev/learning/testing folder.

This "jasmine-standalone" folder is going to be the main project folder for your JavaScript calculator, so let's rename it from jasmine-standalone-2.x.x to jsCalc and open it in a text editor.

The jasmine-standalone files come with example JavaScript files and test specs called PlayerSpec.js, SpecHelper.js, Player.js, and Song.js. Delete those files, since we'll be creating our own. Also, open SpecRunner.html and delete the 4 <script> tags under the "include source files here" and "include spec files here" comments. Now your SpecRunner.html should look like this:

<!-- SpecRunner.html -->

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Jasmine Spec Runner v2.4.1</title>

    <link rel="shortcut icon" type="image/png" href="lib/jasmine-2.4.1/jasmine_favicon.png">
    <link rel="stylesheet" href="lib/jasmine-2.4.1/jasmine.css">

    <script src="lib/jasmine-2.4.1/jasmine.js"></script>
    <script src="lib/jasmine-2.4.1/jasmine-html.js"></script>
    <script src="lib/jasmine-2.4.1/boot.js"></script>

    <!-- include source files here... -->
    
    <!-- include spec files here... -->

</head>

<body>
</body>

</html>

and your src and spec folders should be empty. Now we have Jasmine set up for our own JavaScript testing purposes!

Write a simple JavaScript calculator function to add two numbers

Create a new file inside the src folder called calculator.js and include a <script> tag referencing it inside SpecRunner.html under the "include source files here" comment:

//  src/calculator.js

function add(x, y) {
    return x + y;
}
...
<!-- include source files here... -->
<script src="src/calculator.js"></script>

NOTE: This is obviously a contrived example, since the add function is so dead simple, but our purpose right now is to learn Jasmine, so it will do for now. Just know that typically you'll be testing more complex functions to make sure your logic is correct.

Add a test file

We have a JavaScript function written, and we need to unit test it to make sure it does everything it's supposed to do (add two numbers together correctly).

In order to do this, we need to add a test file to our spec folder. We'll call it calculator.spec.js, although you could name it almost anything you want. Conventionally you'll see something like foo.spec.js, or bar_test.js, but it's up to you how you name your test files, as long as you stay consistent.

Make sure you add this new test file to SpecRunner.html under the "include spec files here" comment:

...
<!-- include spec files here... -->
<script src="spec/calculator.spec.js"></script>

Jasmine Basics

We'll need to understand some of the basics of how Jasmine works before we're able to write this test file. Feel free to check out the official Jasmine tutorial for the full documentation.

Jasmine, and pretty much any test framework, has a pretty straight-forward syntax. Everything is intended to read practically like English so that your tests become self-documenting and can actually help other developers better understand your source code.

Using describe() Blocks

Jasmine uses a built-in describe block (it's actually a JavaScript function, as you'll see shortly) to separate entire sets of functionality. Since our calculator is going to be so simple, we'll probably put all the mathematic operations inside a single describe block. If we were testing Angular, we might put all tests for a single Angular Service (perhaps a UserService, for example), inside one describe block.

describe looks something like this:

//  spec/calculator.spec.js

describe("Calculator", function () {
    
});

The first parameter is a name for the test "suite", and tells Jasmine (and yourself/other programmers) that everything inside this describe block is dealing with the calculator part of your application.

Using it() Blocks

Another built-in Jasmine block (function) is the it() block. This block represents a single unit test. You can have multiple it() blocks inside of one describe() block, which represent multiple unit tests inside of a "test suite."

The first parameter to the it() block is an explanation of what the function you're testing should do. VERY commonly, it reads like a sentence beginning with the word "should":

describe("Calculator", function () {
    it("should add two numbers together", function () {

    });
});
Using expectations and matchers

Everything above was about setting up a test suite and a single unit test. Now we get to the core of writing the tests: setting expectations and matchers.

All this means is that we can tell the test that we expect something to return a certain result. For example, we can say that we expect true to be true always. (This is like writing out if (true === true){... in JavaScript).

With the built-in expect(value) function, we tell Jasmine we're going to expect that value to match some kind of condition. We use matchers to do this.

By way of example, add an expectation to your unit test like this:

describe("Calculator", function () {
    it("should add two numbers together", function () {
        expect(true).toBe(false);
    });
});

Then open SpecRunner.html in a browser (you can use the brackets Live Preview, or simply double click the file from a Finder window.) You should see something like this:

Then change the expect block so it can pass the test (expect(true).toBe(true)) and open SpecRunner.html again:


Back to testing the calculator

This is a pointless test since it is just testing that the JavaScript language was written correctly, so let's change it to be about the JavaScript calculator we're writing instead. It's important to see your test fail before you make it work, so we'll make it fail again using the add() function from our calculator.js:

describe("Calculator", function () {
    it("should add two numbers together", function () {
        expect(add(1, 2)).toBe(4);
    });
});

You might wonder how this file knows about the add() function. Remember we added calculator.js in SpecRunner.html above our calculator.spec.js, so the browser's JavaScript engine that is reading through the JavaScript included in <script> tags in SpecRunner.html already read the functions from our calculator into memory, so it can use those anywhere else, including our test files.

Watch that test fail, change the expect line to read expect(add(1, 2)).toBe(3);, reload the page and watch it pass. You've written your first passing test!

While this is admittedly a very simplified example and may seem useless, think about the following scenario:

Imagine you've been working on this calculator app for awhile now, and you've now found a reason to add more code to the add() function to make it work under other circumstances, so you make changes to your add() function. Without unit tests written from the past to test the original functionality, you would have to manually go through and test the original add() function to make sure it does what it's supposed to still.

However, since you've written this unit test, all you have to do is run all your tests again (open SpecRunner.html) and make sure your new code didn't break anything from your old expectations!


Add more functionality using the TDD approach

Let's try a Test-Driven Development approach:

Add a test for subtraction
//  spec/calculator.spec.js

describe("Calculator", function () {
    it("should add two numbers together", function () {
        expect(add(1, 2)).toBe(3);
    });
    
    it("should subtract two numbers", function () {
        expect(subtract(3, 2)).toBe(3);  // Wrong, to make sure test is running correctly
        expect(subtract(-10, -1)).toBe(-9);  // not really necessary - just meant to demonstrate multiple expectations inside one it() block
    });
});

You should receive an error that "subtract" is not defined. So let's go define it:

//  src/calculator.js

...
function subtract(x, y) {
    return x - y;
}

Now you should have a failing expectation: "Expected 1 to be 3." Now just change the test to the correct value (expect(subtract(3, 2)).toBe(1);) and watch the tests pass:

Continue this process for multiplication and division

Let's add a multiplication and division test. By default, JavaScript returns Infinity when you try to divide by zero. Let's change it so it returns 0 instead, then write the appropriate test for that.

//  src/calculator.js

function add(x, y) {
    return x + y;
}

function subtract(x, y) {
    return x - y;
}

function multiply(x, y) {
    return x * y;
}

function divide(x, y) {
    if (y === 0) {
        return 0;
    } else {
        return x / y;
    }
}
//  spec/calculator.spec.js

describe("Calculator", function () {
    it("should add two numbers together", function () {
        expect(add(1, 2)).toBe(3);
    });

    it("should subtract two numbers", function () {
        expect(subtract(3, 2)).toBe(1);
        expect(subtract(-10, -1)).toBe(-9);
    });

    it("should multiply correctly", function () {
        expect(multiply(2, 3)).toBe(6);
    });

    it("should divide correctly", function () {
        expect(divide(10, 5)).toBe(2);
    });

    it("should return 0 when dividing by 0", function () {
        expect(divide(1, 0)).toBe(0);
    });
});

Looking at the code from the divide() function, I realize I can re-write that code in a single line using a JavaScript ternary operator. The process of re-writing this code to be more concise is known as refactoring. However, without tests refactoring can be a huge pain because you'll need to manually check everything you've written to make sure everything else still works. But since we've written tests checking the conditions upon which we will consider the divide function working, we only need to run our existing tests again and can check immediately if we've broken anything else.

//  src/calculator.js

...
function divide(x, y) {
    return (y === 0) ? 0 : x / y;
}

I refresh SpecRunner.html and make sure my tests all pass, and I know I'm all set to keep coding!


Other Jasmine matchers

In the calculator we only used the .toBe() matcher. However, Jasmine provides us with tons more to use. Instead of repeating everything that's in the Jasmine introduction tutorial, you should take the time now to go check it out and see all the awesome matchers you have at your disposal. For example, you can negate any matcher by simply adding .not before the matcher! (expect(true).not.toBe(false) will run a passing test spec!).


Jasmine Limitations

Jasmine can be used by itself to run plain JavaScript tests in the manner shown. However, due to differences between browsers, you'll sometimes want to test how your JavaScript code is running in each browser to make sure it is compatible across all of them. This becomes a pain, because you need to open the SpecRunner.html file in each browser manually to make sure everything is still passing.

It also can be annoying/inefficient/impossible to pull down the Jasmine standalone files/folders and be expected to move everything of your existing project around to fit their structure. If we have an existing complex AngularJS single-page application, for example, we're not going to want to change around the location of all the Angular files and deal with all the errors that pop up because of it just so we can add unit tests.

In the next part of this series, we'll take a look at Karma, which is a test runner created by the AngularJS team. Karma allows us to use the same Jasmine framework but can test multiple browsers right from the command line, as well as a few other niceties that come built-in.


Conclusion

Make sure to check out the rest of this series on testing, because we've only scratched the surface so far! We'll be covering the use of Karma for additional productivity, how to test Angular components like Services, and how to do end to end testing using Protractor!