JavaScript Unit Testing on MacOS/Windows

A command line unit testing suite complete with it's own unit tests.

-

As a full stack developer, one works with front end javascript to enhance the richness of the users experience. As our code base grows, it's important to employ SOLID principles like dependency inversion to ensure our code is extensible and further, testable. As developers we are constantly working toward automating our build processes without sacrificing speed of development or increasing code complexity. Below follows a short utility I developed to aid in my continuous integration and continuous deployment processes. In combination with a bit of command piping, this framework could allow for the failing of an artifact or build when we've broken a JavaScript unit test.

Running JavaScript in the Terminal

Depending on your OS flavor, you have a few options for executing JavaScript from the command line. For this short library I'll talk about the native options. JavaScriptCore by WebKit on MacOS, and CScript on Windows.

For MacOS there's a little bit of setup that you need to call JavaScriptCore from anywhere. Run the script below to setup command line access to jsc. more, even more

sudo ln -s /System/Library/Frameworks/JavaScriptCore.framework/Versions/A/Helpers/jsc /usr/local/bin

On MacOS, run the tests.js script file with the command below to see if your tests work.

jsc tests.js

On windows the command is:

cscript tests.js

tests.js

Let's talk quickly about what's going on in tests.js. There's a number of functions that come built into JavaScriptCore, like print and load, but in cscript on Windows, these functions don't exist by default. So if you're on Windows, I've filled these functions in for cross platform operation. Next we load in all of our dependencies and run them.

'use strict';

var tests = '';
var print = print || function print (msg) { 
   WScript.StdOut.WriteLine(msg); 
};
var load = load || function load (file) { 
      tests += 
         new ActiveXObject(
            'Scripting.FileSystemObject'
         )
         .OpenTextFile(file, 1)
         .ReadAll();
    };

load('mocks.js'); // mock objs

// scripts & tests
load('rmio.js');
load('rmio.tests.js');
load('testSuite.tests.js');

// load and run testSuite.js
load('testSuite.js');

// if cscript, eval the loaded code.
eval(tests);

TestSuite.js

The test suite itself is pretty slim. It makes available 2 functions to assert equality, and it runs itself. When a test fails, it logs the function and file name to the terminal.

'use strict';

var TestSuite = TestSuite || {};
TestSuite.RunSuite = function(Dependencies) {
   TestSuite._summary_cnt = 0;
   TestSuite._summary_passed = 0;
   // loop through all properties in the test suite
   for (var pkg in TestSuite) { 
      // if the property is a test package
      if (pkg.indexOf('TestPackage') !== -1) {
         // loop through its tests
         for (var test in TestSuite[pkg]) {
            if (typeof TestSuite[pkg][test] === 'function') { 
               // run the test and pass in dependencies
               TestSuite[pkg][test](Dependencies); 
            }
         }
      }
   }
   print(
      '\n' + 'tests ran    : ' + TestSuite._summary_cnt +
      '\n' + 'tests passed : ' + TestSuite._summary_passed
   );
};

TestSuite.AssertEqual = (A, B, TestIsSilent) =>
   TestSuite._log((A === B), !!TestIsSilent);

TestSuite.AssertNotEqual = (A, B, TestIsSilent) =>
   TestSuite._log((A !== B), !!TestIsSilent);

TestSuite._log = (Result, TestIsSilent) => {
   if (!TestIsSilent) 
      TestSuite._summary_cnt++;
   if (Result && !TestIsSilent) 
      TestSuite._summary_passed++;
   if (!Result && !TestIsSilent) { 
      var caller = new Error().stack.split('\n')[1];
      print(
         '\n' + caller.split('@')[0] +
         '\n' + ' - ' + caller.split('/')[1]
      ); 
   }
   return Result;
};

TestSuite.RunSuite(Mocks);

mocks.js

Now that we have an understanding of how the test suite works, we can start coding our unit tests. For example, I wanted to offer a level of functionality for the iPhone X. Specifically to take advantage of the unsafe space caused by its distinctive notch. I'll want to define a set of tests that assure me my function will always correctly return that I'm actually using an iPhone X. The distinguishing characteristics are that it has a userAgent with the word 'iPhone' in it, that its screen is 375 by 812px, and that it has a device pixel ratio of 3.

The TestSuite itself requires that we load our dependencies from the get-go to inject into the libraries I want to test later. There's more we can do here with external libraries like mock browser to limit the need to mock the dom, but for the purposes of this article I've chosen to mock up the simple dom requirements myself.

'use strict';

var Mocks = {
    window: {
        screen: {
            width: 375,
            height: 812
        },
        devicePixelRatio: 3
    },
    navigator: {
        userAgent: 'iPhone'
    }
};

rmio.tests.js

Next, I'll follow a simple pattern to define my unit tests. I'll add a property to the TestSuite object with a property name that includes 'TestPackage' in it. Then I'll define the the tests as named functions in my test package. The TestSuite will loop through these functions and run them each and inject our mocked objects for us.

'use strict';

var TestSuite = TestSuite || {};
TestSuite.RmioTestPackage = { 
   All_Properties_Indicate_An_IPhoneX: function(Mocks) {
      var navigator = JSON.parse(JSON.stringify(Mocks.navigator));
      var window = JSON.parse(JSON.stringify(Mocks.window));

      var result = Rmio.XSupport.IsIPhoneX(navigator, window);
      TestSuite.AssertEqual(result, true);
   },
   OtherTests: function(Mocks) {
   }
};

rmio.js

With my unit test in place, I can write the code and run the tests.

'use strict';

var Rmio = Rmio || {};
Rmio.XSupport = Rmio.XSupport || {};
Rmio.XSupport = {
   IsIPhoneX: function(Navigator, Window) {
      var iOS = /iPad|iPhone|iPod/.test(Navigator.userAgent)
         && !Window.MSStream;
      var ratio = Window.devicePixelRatio || 1;
      var screen = {
         width : Window.screen.width * ratio,
         height : Window.screen.height * ratio
      };
      return (iOS && screen.width === 1125 && screen.height === 2436);
   }
};

testSuite.tests.js

Finally, a test suite wouldn't be complete without a slew of tests that assert the testSuite itself is working correctly.

'use strict';

var TestSuite = TestSuite || {};
TestSuite.TestSuiteTestPackage = { 
   AssertEqualEqualEqual_true: function(Dependencies) {
      TestSuite.AssertEqual(true, true);
   },
   AssertEqualEqualEqual_false: function(Dependencies) {
      TestSuite.AssertEqual(false, false);
   },
   AssertEqualEqualEqual_0: function(Dependencies) {
      TestSuite.AssertEqual(0, 0);
   },
   AssertEqualEqualEqual_1: function(Dependencies) {
      TestSuite.AssertEqual('1', '1');
   },
   AssertEqualEqualEqual_undefined: function(Dependencies) {
      TestSuite.AssertEqual(undefined, undefined);
   },
   AssertNotEqualEqual_false: function(Dependencies) {
      TestSuite.AssertNotEqual('false', false);
   },
   AssertNotEqualEqual_true: function(Dependencies) {
      TestSuite.AssertNotEqual('true', true);
   },
   AssertNotEqualEqual_0: function(Dependencies) {
      TestSuite.AssertNotEqual('0', 0);
   },
   AssertNotEqualEqual_1: function(Dependencies) {
      TestSuite.AssertNotEqual('1', 1);
   },
   AssertNotEqualEqual_undefined: function(Dependencies) {
      TestSuite.AssertNotEqual('undefined', undefined);
   },
   AssertEqualEqualEqual_shouldfail_1_equals_true: function(Dependencies) {
      TestSuite.AssertEqual(
         TestSuite.AssertEqual(1, true, true), 
         false
      );
   },
   AssertEqualEqualEqual_shouldfail_0_equals_false: function(Dependencies) {
      TestSuite.AssertEqual(
         TestSuite.AssertEqual(0, false, true), 
         false
      );
   }
};

Thanks for reading and happy TDD.

Article originally published 2017.12.15

Rich Minchuk

Technology Enthusiast and Wannabe Growth Hacker