lightbulb logo

jsTest JavaScript unit testing tool

This article is about jsTest, a JavaScript unit testing tool. Although this tool could be used in many JavaScript testing scenarios, it is specifically designed for writing unit tests as part of the Test Driven Development (TDD) iteration cycle: write a test, make the test pass, refactor, repeat.

What makes jsTest different from other JavaScript unit testing tools like this and this is that it can be used as a command line tool or an Eclipse plug-in. The tool itself is written in Java so it will run on any platform with a Java VM. It uses Mozilla's Rhino interpreter to run the JavaScript tests.

If you want to try out the examples, first download jsTest here.

First off, this is how to run some tests with jsTest, assuming your tests are in a file called "mycode.test.js". I'm going to use the ".test.js" extension to differentiate test files from non-test files but that is optional.

java -cp jstest-1.0.3.jar;js.jar org.thinkpond.jstest.Main mycode.test.js

That's quite a lot of typing. In practice this command would be part of a script or makefile or hooked up to a menu in your IDE, whatever makes the tests as convenient as possible to run. The file js.jar is the JavaScript interpreter used to run the tests. You can find it in the zip file located here. Version 1.6R5 is the one I use. Download it and put it in the same directory as jstest.jar.

Here is a simple test to get started.

function test_square_root_of_4_is_2()
{
  Test.areEqual(2, squareRoot(4));
}

Going through it line by line, line 1 identifies the test by giving it a name, "test_square_root_of_4_is_2". A test is just a normal JavaScript function. jsTest knows it is a test because the function name is prefixed with "test_". You can put functions without the "test_" prefix in your test files but they will not be run as tests.

Line 3 is an assertion. It asserts that the result of the squareRoot function should be 2. The convention for assertions is to put the expected value on the left side and the tested (actual) value on the right. This is consistent with other testing tools like JUnit.

Assuming that squareRoot evaluates as intended, the test will pass and the output from the test run will look like this.

Tests: 1
Failures: 0
Errors: 0
Warnings: 0 

Going through this line by line, line 1 says that one test was run. Line 2 says that no tests failed (i.e. they all passed). Line 3 says there were no errors. There is a subtle difference between an error and a failure. A failure is always an error. But an error would also result, for example, if the file mycode.test.js could not be opened. Then there would be one error and no failures. Line 4 says there were no warnings. We'll visit warnings again later.

Because there were no errors in this example, the jsTest process will exit with an error level of zero. This is the convention that command line tools use to signal that they have succeeded. However, if the test run failed, for example if the assertion failed or if the test file could not be found, the error level would be non-zero. This would cause a script or makefile to terminate early with an error.

A test file can contain more than one test, in which case all the tests will be run. Here is an example.

function test_square_root_of_4_is_2()
{
  Test.areEqual(2, squareRoot(4));
}

function test_square_root_of_9_is_4()
{
  Test.areEqual(4, squareRoot(9));
} 

Notice that the test beginning on line 6 is actually incorrect. The square root of 9 is not 4, it is 3. So, again assuming squareRoot works correctly, the output would look like this.

mycode.test.js(8): areEqual failed: expected "4" got "3"
---------
Tests: 2
Failures: 1
Errors: 1
Warnings: 0 

Line 1 tells us about the failure. It tells us the file containing the error and the line number of the failing assertion (line 8). It tells us that an assertion of type "areEqual" failed. There are other types of assertion as we will see later. Line 3 tells us there were 2 tests in total. Lines 4 and 5 tell us that one of the tests failed.

In most cases we want to put the tests in a separate file from the code that is being tested. Lets say we are writing some code for a web site. We don't want our users to have to download the tests when they download the web pages. A "requires" statement lets the tests operate on code in another source file.

Test.requires("mycode.js");

function test_square_root_of_4_is_2()
{
  Test.areEqual(2, squareRoot(4));
} 

Line 1 has the effect of copying and pasting the text of mycode.js into mycode.test.js, allowing us the keep the code in mycode.js separate from the tests in mycode.test.js.

Many unit testing tools have the concept of fixtures and setup functions and jsTest is no exception. However, jsTest does not represent fixtures as objects. Instead it runs each test in its own sandboxed environment which means tests can use global variables without fear of affecting the results of other tests.

Consider this example. Which of these tests fails?

var a = 3; 

function test_1() 
{ 
  Test.areEqual(3, a); 
  a = 4; 
} 

function test_2() 
{ 
  Test.areEqual(3, a); 
  a = 4; 
}

It may seem that it depends on which order the tests are run (which is actually undefined). If test1 runs first, it will pass but it will change the value of the variable "a", causing the second test to fail or vice-versa.

But they actually both pass! The reason is because they both run in their own sandboxed environment and a change to the value of "a" in one test is invisible to any other tests. So the variable "a" has the value 3 on entry to both tests. On exiting both tests, the new value of "a" is lost forever.

This is how jsTest does fixtures. How about setup functions? jsTest does not need functions for setup. Just write the setup at top-level scope, like this.

var httpRequest = createHttpRequest(); 

function test_http_request_is_asynchronous() 
{ 
... 
} 

function test_http_request_uses_post_method() 
{ 
... 
} 

The function "createHttpRequest" is evaluated twice, once before each test. So put your common setup code at top-level scope.

Now lets return to warnings.

function half(a)
{
  var r = a / 2;
  Test.warning("r is " + r);
  return r;
}

function test_half_of_5_is_2()
{
  Test.areEqual(2, half(5));
}

Running this test results in the following output.

a.test.js(10): warning: r is 2.5
at a.test.js:4
a.test.js(10): areEqual failed: expected "2" got "2.5"
---------
Tests: 1
Failures: 1
Errors: 1
Warnings: 1

Line 1 shows the string that was passed into Test.warning. So a warning is sort of like an assertion but unlike an assertion, it does not cause the test to fail. Together with line 2, it tells us that the failure happened on both line 10 and line 4. What does that mean? This is a call stack trace. It means that half was called on line 10, which in turn called Test.warning on line 4. This helps us follow the chain of calls that resulted in the warning.

You should not leave warnings in your tests. A test should either pass or fail. If it gives a warning, it is hard to know whether to continue work or fix the test. Warnings are useful as a debugging tool though. When you can't figure why a test fails, you can sprinkle your code with warnings to see what is going on.

The fail function is similar to the warning function except it causes a test to fail. It is useful when a test needs to verify something that is not easily expressed as an assertion. Our first example could be rewritten like this.

function test_square_root_of_4_is_2() 
{ 
  if (2 != squareRoot(4)) 
  { 
    Test.fail("I'm a failure!"); 
  } 
} 

Finally lets take a look at the other available assertions. We have already seen "areEqual". These are the others.

Test.isNull(a);
Test.isNotNull(a);
Test.isTrue(a);
Test.isFalse(a);
Test.areEqual(a, b);

Most of these should be pretty clear. If you can't guess what they do, try them out!

Lastly, in my next article I will cover how jsTest supports mock objects.

Download jsTest here.