Unit Testing JavaScript on Windows

JavaScript is playing an increasing role in modern applications. While it is commonly used in Web applications to implement client-side scripting, JavaScript is finding its way into more applications every day as it becomes a defacto standard for application scripting. If you practice test-driven development, you need a way to write unit tests around your JavaScript just as much as you need a way to write unit tests against your C++, C# or Java code. In this post I’ll describe a simple mechanism for writing unit tests against JavaScript on Windows via Windows Script Host.

There are existing unit test frameworks for JavaScript, but they rely on a web browser for their execution environment. This can present some problems if you are trying to run these unit tests in an automated fashion during your nightly build. How do you have the web browser report the results back to the build system? If your build system runs under cron on a unix platform, how do you get a web browser to run without a display? On the Windows platform, we can use Windows Script Host to run our JavaScript code in a controlled environment that loads the test framework, appropriate test doubles for our system under test, the system under test and the unit tests for execution.

Let’s take a look at the IncludeAnalyzer object from my post on Windows Script Host and see how we could write unit tests for that object. IncludeAnalyzer collaborates with three other objects: an instance of the COM object Scripting.FileSystemObject, TextStream objects returned from the file system object and the global WScript object. During testing, we can take care of the FileSystemObject and TextStream coupling by providing our own test doubles. The test doubles must implement the same methods used by the system under test. Here is the test double for the FileSystemObject:

    1 function FakeFileSystemObject()
    2 {
    3     this.FileExistsFakeResult = new Object;
    4     this.OpenTextFileFakeResult = new Object;
    5 }
    6 
    7 FakeFileSystemObject.prototype.FileExists = function(path)
    8 {
    9     return this.FileExistsFakeResult[path];
   10 }
   11 
   12 FakeFileSystemObject.prototype.OpenTextFile = function(path, access)
   13 {
   14     return this.OpenTextFileFakeResult[path];
   15 }

Here is the test double for the TextStream object:

    1 function FakeTextStream()
    2 {
    3     this.AtEndOfStream = false;
    4     this.ReadLineFakeResult = [];
    5     this.m_line = 0;
    6 }
    7 
    8 FakeTextStream.prototype.ReadLine = function()
    9 {
   10     this.AtEndOfStream = (this.m_line >= this.ReadLineFakeResult.length);
   11     return this.ReadLineFakeResult[this.m_line++];
   12 }
   13 
   14 FakeTextStream.prototype.Close = function()
   15 {
   16 }

Now we just need a way to decouple the IncludeAnalyzer from the WScript.Echo method. The simplest way to do this is to introduce a level of indirection and invoke a method on IncludeAnalyzer instead of invoking the method on WScript directly. Due to the dynamic nature of JavaScript, we can replace this method on the system under test to sense when it is called. Here is the code for our test:

    1 function FakeTextStreamContents(contents)
    2 {
    3     var stream = new FakeTextStream;
    4     stream.ReadLineFakeResult = contents;
    5     return stream;
    6 }
    7 
    8 function FakeFileExists(fso, name, streamContents)
    9 {
   10     fso.FileExistsFakeResult[name] = true;
   11     fso.OpenTextFileFakeResult[name] = FakeTextStreamContents(streamContents);
   12 }
   13 
   14 function TestIncludes()
   15 {
   16     var fso = new FakeFileSystemObject;
   17     var analyzer = new IncludeAnalyzer;
   18     analyzer.m_includeDirs = [ "." ];
   19     analyzer.m_fso = fso;
   20     var output = [];
   21     analyzer.Echo = function(text)
   22     {
   23         output.push(text);
   24     }
   25 
   26     FakeFileExists(fso, "stdio.h",
   27         [
   28             "// this is stdio.h"
   29         ]);
   30     FakeFileExists(fso, "Foo/Foo.h",
   31         [
   32             "#include <stdlib.h>"
   33         ]);
   34     var input = FakeTextStreamContents([
   35         "#include <stdio.h>",
   36         "#include <Foo/Foo.h>"
   37     ]);
   38 
   39     analyzer.ProcessFile(input);
   40     AssertEqual("stdio.h", output[0]);
   41     AssertEqual("Foo/Foo.h", output[1]);
   42     AssertEqual("  stdlib.h", output[2]);
   43 }

We start with a few simple helper functions that were created to eliminate some duplication in the test setup. We replaced the FileSystemObject used by the production code with our test double and we replaced the implementation of the Echo method to capture the output of the code. After configuring the fake input files, we invoke the system under test and verify the output. AssertEqual comes from our simple unit test framework in a WSF file:

    1 <?XML version="1.0" standalone="yes" ?>
    2 <job id="main">
    3     <?job debug="true" ?>
    4     <script language="JScript" src="IncludeAnalyzer.js" />
    5     <script language="JScript" src="FakeFileSystemObject.js" />
    6     <script language="JScript" src="FakeTextStream.js" />
    7     <script language="JScript" src="TestIncludeAnalyzer.js" />
    8     <script language="JScript">
    9         function TestFailure()
   10         {
   11         }
   12 
   13         function AssertEqual(left, right)
   14         {
   15             if (left != right)
   16             {
   17                 WScript.Echo(left + " != " + right);
   18                 throw new TestFailure;
   19             }
   20         }
   21 
   22         function RunTests()
   23         {
   24             try
   25             {
   26                 TestIncludes();
   27             }
   28             catch (e)
   29             {
   30                 return 1;
   31             }
   32             return 0;
   33         }
   34 
   35         WScript.Quit(RunTests());
   36     </script>
   37 </job>

Using these same techniques, we can write unit tests for JavaScript code that executes in the context of a browser, in the context of an application, or as a WSH script. We can make small modifications to existing code in order to fit unit tests onto that code, or we can create new JavaScript code in a test-driven development style. The dynamic nature of JavaScript permits easy ways to decouple objects from their environment to test them in isolation. It is a simple matter to create the necessary test doubles for global objects like window or document for browser based JavaScript.

You can download the code described in this post at the link below.

Download TestAnalyzeIncludes.zip

Leave a comment