There’s something about the testSum
example that I really don’t like. I think the unit tests are good, and I think the example does a good job of demonstrating what types of components to include in a unit test. The problem is that testSum
does a great job of teaching the basics of unit testing, but it doesn’t show you how to test real code. Unit tests for a sum
function might not be hard to write, but how would you write tests for a real program such as kittenbook?
The first step is to think about the kittenbook code in units. The entire kittenbook project is a lot more complex than sum
, but each unit (function) can be just as simple. In fact, this is one of the big advantages of writing unit tests: For code to be unit testable, the code must be written in small, manageable units. Code that is written in small, manageable units is cleaner, more organized, and generally less prone to error. Before we organized the kittenbook code into functions, it would have been almost impossible to test. We will not try to test all of kittenbook at once, but we will test each function in isolation.
The good news is that Grunt can help us with our unit tests. We will be using a testing library called Jasmine to write and run our unit tests. Jasmine basically provides testing utility functions such as test
, plus a lot more. To get Jasmine set up, you first need to install grunt-contrib-jasmine
(which installs Jasmine and all its dependencies). Then you need to add a Jasmine task to Gruntfile.js
(see Listings 12.2 and 12.3).
## First navigate to the kittenbook directory, then run the following command
~/project/kittenbook $ npm install grunt-contrib-jasmine --save-dev
module.exports = function(grunt) {
grunt.initConfig({
...
jasmine: {
test: {
src: ['js/values.js', 'js/prompt.js', 'js/getImages.js',
'js/replaceImages.js', 'js/main.js'],
options: {
specs: 'test/*.js'
}
}
},
...
});
// Load Grunt plugins
...
grunt.loadNpmTasks('grunt-contrib-jasmine'),
...
};
Listing 12.3 shows only the new parts of Gruntfile.js
. The new Jasmine task has familiar options. We tell Jasmine where to find the source code with the src
attribute. I have listed the files in the same order as in the concat
task because I found that this avoided some problems when trying to run the tests. Next, we tell Jasmine where to find the test files with the spec
attribute (inside the options
object). You need to create a new directory called test
, which is where we will be putting all our test files. After you have done that, try running grunt jasmine
from the kittenbook
directory on the command line. You should get a warning message that says something like this: “Warning: No specs executed, is there a configuration error? Use --force to continue.” That means the Jasmine task is correctly set up, but Jasmine can’t find any tests to run. When you actually have a test to run, that warning will go away. So let’s write our first test for kittenbook.
Specs
Unit tests are often called specs (short for specifications) because they describe in detail what the software can and cannot do. In the last chapter, you learned about writing a specification as a part of planning. Writing unit tests is one good way to write a specification. If all the unit tests are passing, the software meets the specification.
I think functions with simple input and simple output are the easiest functions to test, so we will start with one of those: validatePhoneNumber
takes a string as input and then outputs a Boolean. You can write several tests for a single function; each test should be testing one small feature of the function. Our first test checks whether validatePhoneNumber
returns a Boolean, as shown in Listing 12.4. You can see a few of Jasmine’s utility functions as well. These functions are here to help, so try not to feel intimidated or confused.
The describe
function describes what is being tested. describe
accepts two arguments: a string that describes what is being tested and a function that contains the tests being described. In Listing 12.4, you can see a describe
inside a describe
, which is perfectly okay. The outer describe
says that we are testing functions found in prompt.js
, and the inner describe
says that we are testing the validatePhoneNumber
function.
The it
function runs a single test. I would have picked a more descriptive name (such as test
), but no one asked me. it
accepts two arguments: a string describing the single test and a function containing the test code. The string usually starts with the word should, so the code reads “it should return a boolean.”
The expect
function tests an expectation. If your expect
passes, your test passes. If your expect
fails, your test fails. If you don’t have an expect
, your test always passes, so you should always have at least one expect
within each it
function. The expect
is always accompanied by another utility function (such as toBe
), so the expect
line of code reads something like this: “Expect [actual result] to be [expected result].” If the actual result doesn’t meet your expectations, the test fails.
describe('prompt.js', function() {
describe('validatePhoneNumber', function() {
it('should return a boolean', function() {
var result = validatePhoneNumber('23456'),
expect(typeof result).toBe('boolean'),
});
});
});
The test code in Listing 12.4 is describing the validatePhoneNumber
function in the prompt.js
file. Only one test has been written so far, and it states that validatePhoneNumber
should return a Boolean. Within the test, validatePhoneNumber
is called, and the value it returns is saved in the result
variable. The next line says that we expect the (data) type of result
to be a Boolean (typeof
is an operator in JavaScript, which gives the data type of its operand). Note that we don’t care whether the result
is true
or false
, as long as it is a Boolean. If the data type of result
is a Boolean, the test passes; if it is not a Boolean, the test fails and we know that something is wrong with our application code. Run the tests from the command line using grunt jasmine
, and you will see that our test fails. This is because validatePhoneNumber
uses the String method match
(which returns an array or null
) instead of the Regular Expression method test
(which returns a Boolean). Our unit test helped us find a bug in our code. After the bug is fixed, you should see something like Figure 12.3.
18.188.91.44