Getting started

Aaron Jacobs edited this page Nov 12, 2014 · 6 revisions

This document contains a tutorial for writing your first test using the Google JS Test framework. If you haven't yet, you'll need to first follow the instructions on the Installing page to get the gjstest tool set up on your system.

<wiki:toc />

Introduction

Google JS Test consists of the following parts:

  • A unit testing framework that allows you to register test functions that express expectations using functions like expectEq or expectThat, and matchers like lessThan(2) or containsRegExp(/taco/).

  • Built-in mocking capabilities, allowing you to create mock functions or class instances whose behavior you control in the test. Any expectations registered with the mocking framework are automatically checked by the unit testing framework.

  • A tool called gjstest that allows you to run the tests you write and see whether they passed or failed.

The sections below take you through writing a toy class and testing it with Google JS Test.

Code under test

For the purposes of this tutorial, let's assume that we're working on a file called some_functions.js with containing several functions we want to test. Create the file some_functions.js and put the following code in it:

// Return some interesting words.
myproject.getSomeWords = function() {
  return ['Hello', 'World'];
};

// Add the supplied numbers and then call back with the result.
myproject.addNumbersAndCallBack = function(a, b, resultCallback) {
  var result = a + b;
  resultCallback(a + b);
};

These are just toy functions; you'd probably have something more interesting in your project.

Note that the object myproject (serving as a namespace here) has not been defined within this file. Let's suppose its definition is in a separate file, so that it can be shared with other code in your project. Add the following to a file called namespace.js:

var myproject = {};

Writing tests

Now we want to write some tests for the functions defined in some_functions.js, which you created above. Create a new file called some_functions_test.js and put the following into it. (Note that there's nothing special about the filename some_functions_test.js; it's just a convention.)

//////////////////////////
// getSomeWords
//////////////////////////

function GetSomeWordsTest() {}
registerTestSuite(GetSomeWordsTest);

addTest(GetSomeWordsTest, function ReturnsCorrectWords() {
  var words = myproject.getSomeWords();

  // Assert directly what the words should be.
  expectThat(words, elementsAre(['Hello', 'world']));

  // Note that you could have also done so as follows, but it doesn't give
  // error messages that are as nice.
  expectEq(2, words.length);
  expectEq('Hello', words[0]);
  expectEq('world', words[1]);
});

//////////////////////////
// addNumbersAndCallBack
//////////////////////////

function AddNumbersAndCallBackTest() {
  // Create a mock function and store it in a place accessible to the test
  // functions below. A new one will be created for each test method.
  this.resultCallback_ = createMockFunction();
}
registerTestSuite(AddNumbersAndCallBackTest);

addTest(AddNumbersAndCallBackTest, function HandlesPositiveNumbers() {
  var a = 17;
  var b = 23;

  // Assert that the mock callback will created above will be called with the
  // appropriate result.
  expectCall(this.resultCallback_)(40);

  // Call the function being tested with the appropriate arguments, including
  // our mock callback.
  myproject.addNumbersAndCallBack(a, b, this.resultCallback_);
});

addTest(AddNumbersAndCallBackTest, function HandlesNegativeNumbers() {
  var a = -17;
  var b = -5;

  expectCall(this.resultCallback_)(-22);

  myproject.addNumbersAndCallBack(a, b, this.resultCallback_);
});

addTest(AddNumbersAndCallBackTest, function ThrowsErrorForStringArg() {
  // Make sure that if we give a string instead of a number for the first
  // argument, the function under test throws an appropriate error.

  var trivialCallback = function() {};
  var callWithString =
      function() {
          myproject.addNumbersAndCallBack('foo', 17, trivialCallback);
      };

  expectThat(callWithString, throwsError(/TypeError.*must be a number/));
});

This file defines two test suites, one for each function we're testing. A test suite is a way to group logically related tests that may need to share setup code. For example, the constructor of AddNumbersAndCallBackTest creates a mock function that can be used as a callback in its test methods if desired.

Notice a few things about these tests:

  • The ReturnsCorrectWords method of GetSomeWordsTest makes use of expectThat with the matcher elementsAre in order to assert something about the contents of an array. This is a more concise way of expressing the same thing using several expectEq statements. It also offers the advantage of better error output in the event of a failure; you'll see an example of this below. See the Matchers page for more info about matchers.

  • The constructor for AddNumbersAndCallBackTest creates a mock function and attaches it to the resultCallback_ property of its this object. Each test method on the class can access this.resultCallback_ to get ahold of the mock callback it created.

  • The first two test methods of AddNumbersAndCallBackTest make use of expectCall to express that the mock callback should be called by the function being tested with a particular value. If that doesn't happen, the test will fail. See the Mocking page for more info on mocking, including topics like how to define actions your mock functions should perform.

  • AddNumbersAndCallBackTest.ThrowsErrorForStringArg makes use of another interesting matcher – throwsError matches functions which, when invoked, throw an error matching a supplied regular expression. This allows us to assert that myproject.addNumbersAndCallBack throws an appropriate error when we give it a bad argument.

Running tests

Now we're ready to run the tests we wrote above, making sure that the functions we're testing do the write thing. We do this using the gjstest tool, which we must tell where to find the code. Run the command below:

gjstest --js_files=namespace.js,some_functions.js,some_functions_test.js

Note that we have to give all of the JS files needed by our test and the code it is testing, and we have to do so in order. If we left out namespace.js or put it after some_functions.js, we'd find that the first time our code accesses myproject it gets an undefined object error.

You should see output like the following:

[----------]
[ RUN      ] GetSomeWordsTest.ReturnsCorrectWords
some_functions_test.js:12
Expected: is an array or Arguments object of length 2 with elements matching: [ 'Hello', 'world' ]
Actual:   [ 'Hello', 'World' ], whose element 1 doesn't match

some_functions_test.js:18
Expected: 'world'
Actual:   'World'

[  FAILED  ] GetSomeWordsTest.ReturnsCorrectWords (4 ms)
[----------]

[----------]
[ RUN      ] AddNumbersAndCallBackTest.HandlesPositiveNumbers
[       OK ] AddNumbersAndCallBackTest.HandlesPositiveNumbers (1 ms)
[ RUN      ] AddNumbersAndCallBackTest.HandlesNegativeNumbers
[       OK ] AddNumbersAndCallBackTest.HandlesNegativeNumbers (0 ms)
[ RUN      ] AddNumbersAndCallBackTest.ThrowsErrorForStringArg
some_functions_test.js:64
Expected: is a function that throws an error matching /TypeError.*must be a number/
Actual:   function (), which threw no errors

[  FAILED  ] AddNumbersAndCallBackTest.ThrowsErrorForStringArg (0 ms)
[----------]

[  FAILED  ]

It turns out that we have some bugs in our code. Notice how the first error message output in GetSomeWordsTest.ReturnsCorrectWords gives nicer output than the second—it shows the entire array, which is extra important if the array is of the wrong length.

Go ahead and fix these bugs, it's left as an exercise to the reader. You'll want to fix the capitilization on the second word returned by getSomeWords, and add type checking logic to addNumbersAndCallBack. Once you do, you should be able to run the gjstest command given above again, and see that your tests pass.