Unit Testing in GAS Part 5: Testing Objects and Arrays

If you're brand new to unit testing, start with the first post in this series to get caught up.

Part 1: QUnit Setup
Part 2: Simple Tests
Part 3: Adding and Updating Functions
Part 4: Error Handling
Part 5: Testing Objects and Arrays


It's time to dive into deeper equality situations with objects and arrays. Every test we've written so far has used a non-strict comparison. In other words, we've only been checking value but not type. This is particularly important in JavaScript because of how it handles truthy and falsy values.

For instance, if you were to write a test checking the equality of 0 and undefined, what would you expect? They're different, right? If you want to try it yourself, you can write a quick test:

QUnit.test('truthy and falsy values', function() {
    equal(0, undefined, 'both are falsy values, but unequal types') // pass
    deepEqual(0, undefined, 'both are falsy values, and unequal types') // fail
})

QUnit passes the first test because both 0 and undefined are falsy - the values are the same, but the type is different. Using equal as the assertion only checks against the value of the actual and expected arguments. This is where deepEqual helps. Instead of checking values only, deepEqual performs a strict check of both the value and type of the arguments.

Get the Source Code

Here's the completed source for this post.

Objects and Arrays

We have only looked at simple values - numbers and strings. In this post, we'll look at using deepEqual and propEqual to test objects and arrays. Rather than jumping right into testing our Calcs class, let's start with two simpler examples. Start by adding this to your tests.gs file:

// function calcTests() { ... }

// Create a new test wrapper to keep things neat
function objectTests() {
    QUnit.test('Object and array basics', function() {
        var array = [1,2,3,4];
        deepEqual(array, [1,2,3,4], 'the arrays are equal');
    });
}

This is the first time we've defined a variable inside a Qunit.test instance. Each test is a function, so it can have block-scoped variables used in the test. These variables do not affect other functions in the wrapper. Eventually, we will be retrieving exisitng objects and arrays to test, but for now, we'll define them with each test as necessary.

Because we're defining a new wrapper, you need to go to config.gs and add objectTests() to the tests() wrapper for these new tests to run:

function tests() {
    console = Logger; // Match JS
    calcTests(); // Collection of tests on the Calcs class.
    objectTests(); // new tests for checking objects and arrays
}

This is personal preference, really...there is nothing saying you cannot include these checks in the calcTests wrapper we're using, but I find it helpful to break out tests into similar groups.

Reload the web app and you'll see a new line passing the array deepEqual check we just wrote. Let's do the same thing for an Object:

function objectTests() {
    // ... array check
    deepEqual({a: "hello", b: "world"}, {a: "hello", b: "world"}, 'These objects are equivalent');
}

This test will also pass because the objects have strict equality with one another. deepEqual is recursive, meaning it will check for equality even within nested objects:

function objectTests() {
// ... array check
// ... shallow object check
deepEqual(
    { 
        a: "hello",
        b: "world",
        c: {
            aa: "foo",
            bb: "bar"
        }
    }, {
        a: "hello",
        b: "world",
        c: {
            aa: "foo",
            bb: "bar"
        }
    }, 'Nested objects can be tested, too'); 
}

Checking Constructed Objects

Checking object constructors is complicated. You cannot just define a matching object in the function because deepEqual checks the constructor along with the value. Rather than testing the entire object, it is better to check each part of the object.

This follows with the unit testing philosophy - test the smallest possible pieces of your code. If you want to test the structure of the object, we can assign a variable to an object with the desired properties and test our Calcs object against it with propEqual.

To help with flow control, I've added an init() method Calcs which will return the entire object. It doesn't matter a whole not right now, but it will in future posts.

var Calcs = (function() {
    const init = function() { 
        return this;
    }
    // rest of Calcs
})

From now on, when we need to instantiate Calcs, we'll use Calcs.init().

To test obect properties, let's add a variable with a known structure to use as our expected value. Then, we'll call Calcs.init() to get the full object back to compare properties.

function objectTests() {
    // ... array, shallow, and deep object checks
    // Model the structure of the Calcs object
    var testCalcsClass = {
      init: function() {},
      name: "calculation methods",
      about: function() {},
      author: function() {},
      add: function(a, b) {},
      isNumber: function(val) {},
      addArray: function(arr, int) {},
    } 
    // .. array, object checks
    propEqual(Calcs.init(), testCalcsClass, 'The constructed object has the expected structure.');
}

propEqual returns true because the properties of both are the same. Calling deepEqual will cause a failure because it checks the properties and the object constructor. Our expected value wasn't created with a constructor like the actual and the test will fail.

Why might this type of check be important?

If your object returns the wrong type of value, propEqual will fail. For example, changing init to a string value in your expected object will fail when compared with Calcs.init() because it's expecting a function, not a string.

Using propEqual on your classes can help prevent type errors down the line by ensuring each property matches the expected type. This kind of check, where you specify an expected structure, is called mocking and we'll look at that in a future post.

Testing Returned Values

What about functions or methods that return structured data? We can use deepEqual to check the returned values. We're going to add a method to Calcs which accepts an array and integer and returns an array with each value increased by the specified amount. Here's the test we'll run:

QUnit.test('Test array calculations', function() {
    equal(Calcs.addArray(2, 2), false, 'Param 1 is not an array');
    equal(Calcs.addArray([1,2,3], 'dog'), false, 'Param 2 is a string');
    deepEqual(Calcs.addArray([1, 2, 3], 2), [3, 4, 5], 'The returned array is correct');
  });

Our test defines three checks that need to pass:

  1. The first parameter is an array,
  2. the second parameter is a number,
  3. and the returned array is equal to the expected value.

Our method needs to accept an array and a number to add to each value in the array. We should get a new array back with the updated values.

const addArray = function(arr, int) {
    // Check the params
    if (!Array.isArray(arr)) { return false }
    if (typeof int !== 'number') { return false }

    var addArr = arr.map(function(val) { return val + int })

    return addArr;
}

return {
    // rest of return
    addArray: addArray
}

If you reload your web app, all tests should pass. This could also be extended with throws to check for custom error messages like we did back in part 4.

Put it into practice

It's easy to get sucked into thinking you need to check for exact data, particularly with Objects and Arrays. With unit testing, remember that you're checking that a piece of your code does what it's designed to do with any data. Running tests on generic structures gives you a clear idea of what any individual part of your application does. Use propEqual to test mocked objects for structure.

Summary

  • equal does a soft comparison (==) and deepEqual uses a strict check (===).
  • deepEqual also checks constructor values for Objects.
  • propEqual compares Object properties (structure) without considering the constructor.

Unit Testing in GAS Part 4: Error Handling

If you're brand new to unit testing, start with the first post in this series to get caught up.

Part 1: QUnit Setup
Part 2: Simple Tests
Part 3: Adding and Updating Functions
Part 4: Error Handling
Part 5: Testing Objects and Arrays


Up until now, our Calcs class has handled errors with simple true and false flags. That's not helpful to the user. At this point, we're ready to begin defining and testing custom errors in our functions. In this post, we're going to throw an error when the add() method receives an invalid input. To keep it simple, we're going to call anything other than the number type invalid and return an error.

Get the Source

You can see the completed source for this part on GitHub.

QUnit throws

The throws assertion is more complex than the ok, equal, notEqual methods we've looked at already. throws will call a function and then can have one of four possible expected params:

  1. An Error object
  2. An Error constructor to use ala errorValue, instanceof, or expectedMatcher
  3. A RegExp that matches (or partially matches) the String representation
  4. A callback function that must return true to pass the assertion check.

With throws, we are able to define not only an error to test, but the kind of error that's returned and even the message received by the test. This is helpful for testing functions that can throw several different types of errors.

We'll start by using the built in Error and TypeError and finish by writing our own CustomError class that you can extend yourself.

Write Failing Tests

To begin, add a new block of tests in tests.gs. Four of these will fail at first and our code will be written to pass each one.

QUnit.test('Checking errors', function() {
    throws(function() { throw "error" }, "throws with an error message only");
    throws(function() { throw new Error }, Error, 'The error was a generic Error');
    throws(function() { throw new CustomError() }, CustomError, 'Creates a new instance of CustomError');
    throws(function() { throw new CustomError("you can't do that!") }, "you can't do that!", "Throws with a specific message");
    throws(function() { throw new CustomError() }, function(err) { return err.toString() === "There was a problem." }, 'When no message is passed, the default message is returned.');
    throws(function() { throw new CustomError("You can't do that.") }, function(err) { return err.toString() === "You can't do that." }, 'Error.toString() matches the expected string.');
  });

When writing your tests, the biggest mistake is that the first parameter must be a function call which throws your error. This is becuase it has to get the returned value to pass to the expected parameter.

When you run your tests by reloading the webapp, the first two assertions will pass because they're handled by the browser. You'll get failures for anything calling CustomError because it doesn't exist yet.

Build the CustomError

We need to create an error called CustomError that does four things:

  1. Raises an instance when called (assertion 3)
  2. Takes a message parameter (assertion 4)
  3. Return the default message if not passed (assertion 5)
  4. Includes a toString method to retrieve the passed message in a callback (assertion 6)

Create a new script file called CustomError and place the following code inside:

var CustomError = function(message) {
    this.message = message || "There was a problem.";
}

CustomError.prototype.toString = function() {
    return this.message;
}

This is scoped globally instead of namespaced (like the Calcs class) because it doesn't access any restricted services in the Apps Script environment. Any class or method can now access and raise this custom error.

If you re-run your test, all assertions should now pass. Now that it is available, we can go back and start using this error in our Calcs class.

Error Handling in the Class

Because the native Error object is always available, we can access those at any point. In calculations.gs, let's not just return false in our function, let's throw a custom TypeError with a message. Our Calcs.add() test block needs to be modified. I'm going to delete a test that no longer applies because we're going to move away from checking with equal. The old line is commented out:

QUnit.test('Checking the `add` method in Calcs', function() {
    ok(Calcs.add(1, 1), 'The method is available and received two variables.');
    equal(Calcs.add(2, 2), 4, 'When 2 and 2 are entered, the function should return 4.');
    // equal(Calcs.add('hello', 2), false, 'When a non-number is added, the function will return false.');
    throws(function() { Calcs.add('foo', 2) }, TypeError, 'When a non-number is passed in the first param, the function will return a TypeError.');
    throws(function() { Calcs.add(2, 'bar') }, CustomError, 'When a non-number is passed in the second param, the function will return a CustomError.');
  });

To pass our tests, we want to update Calcs.add() to throw a TypeError if the first param is not a number and a CustomError if the second is not a number.

Here's a refactored version of the add() method which will pass the test we just wrote:

// ... rest of Calcs
const add = function(a ,b) {
    if(!isNumber(a)) { throw new TypeError }
    if(!isNumber(b)) { throw new CustomError('This deserves a custom message.'); }
    return a + b
}
// ...

This refactor checks a and b independently and throws the specific error to satisfy the assertion statements in the tests. If you run your tests, all assertions should now pass.

The throws method is a powerful tool for testing your exception handling. At a minimum, using the broswer errors can help you give useful information to your users when exceptions occur. throws helps you confidently address each error appropriately before your users run into problems.

Summary

  • throws is an assertion that will raise an error and catch the response for testing.
  • throws expects a function as the first parameter which will raise the error.
  • Define custom errors in the global scope so they can be accessed by all classes and functions.
  • Write a test block for any custom errors you create before adding those raised exceptions to your code.

Unit Testing GAS Part 3: Adding and Testing Functions

If you're brand new to unit testing, start with the first post in this series to get caught up.

Part 1: QUnit Setup
Part 2: Simple Tests
Part 3: Adding and Updating Functions
Part 4: Error Handling
Part 5: Testing Objects and Arrays


We've looked at how to install and configure QUnit and just finished writing some simple tests. In this post, we're going to write a new method, add(a, b) in Calcs which will add the two passed parameters. Then, we'll use testing to check that the params are numbers before returning either the sum or false. We could make this more complex add allow an object (or array), but we'll write another method for that later to compare.

Source

Here is the completed source code for this post.

Write the Test

We know our expected output should be a + b, whatever that happens to be. Let's add a test to tests.gs which will help us write a working function:

function calcTests() {
// ...
    QUnit.test('Checking the `add` method in Calcs', function() {
        ok(Calcs.add(1,1), 'The add method should be available in the module.');
        equal(Cals.add(2, 2), 4, 'When 2 and 2 are entered, the function should return 10.');
    });
}

We added a new QUnit.test() method in the calcTests() wrapper which defines two tests for our new function. ok checks that the function is available (and not accidentally privately scoped) and equal adds 2 and 2 expecting 4 as the result. Running the test now will produce a failure, which is what you would expect because we haven't written the method yet.

Open calculations.gs and add the add method. Don't forget to return it!

// ... rest of code ...
    const add = function(a ,b) {
        return a + b;
    }

    return {
        name: name,
        about: about,
        author: author,
        add: add,
    }
})()

Testing for Errors

We've tried, and passed, two tested conditions, both of which follow the expected use of the function. But, what if a user enters something other than a number? We're going to add a helper function to Calcs which will check a value and return true if it is a number, false if otherwise.

Our function will be called isNumber and here are the tests we'll use for this case:

function calcTests() {
    // .. rest of tests ...

    QUnit.test('Checking the `isNumber` method in Calcs', function() {
        equal(Calcs.isNumber(2), true, 'The value entered is a number');
        equal(Calcs.isNumber('foo'), false, 'The value entered is NOT a number.');
        notEqual(Calcs.isNumber('foo'), true, 'The value entered is NOT a number.');
    });
}

In this block, we introduce the notEqual assertion which will pass if the returned value is false. We expect true in notEqual because I expect Calcs.isNumber('foo') to return false, making the assertion true and passing. (It's a little hard to wrap your head around at first.)

Writing tests first means they will fail whenever the web app is loaded. As you write the function to pass the test, you're keeping code concise and focusing on one (and only one) outcome, thereby improving maintainability and clarity of your codebase.

const Calcs = (function() {
    // ... rest of calcs
    const isNumber = function(val) {
        if(typeof(val) === 'number') {
            return true;
        }
        return false;
    }

    return {
        // ... rest of return
        isNumber: isNumber
    }
})

When writing functions to pass tests, first focus on passing. This function could be restructured to use a ternary or some other method of boolean logic, but that doesn't matter right now. We're just focused on satisfying the test conditions. Then we can go back and refactor.

Running your tests should pass all assertions. If not, go back and look at the failures and debug your code.

Handling Private Functions

In certain cases, not all methods need to be exposed to the global namespace. Our isNumber function could certainly be scoped privately because the Javascript core already includes typing (typeof(2) === 'number' // true) which can handle checking.

Testing private methods is tricky and reasons for why you should or shouldn't vary. In applications which compile code with a build process, there are methods for testing private methods. In Apps Script, there is no such build step, so testing private functions becomes more difficult. Here are some considerations:

  • Why is the function private? If it is performing a necessary task within the class, consider exposing it to the user.
  • Keep private functions simple, like our boolean test, and write tests which require the private function to also pass.
  • Use separate helper classes with utility functions that can be tested separately.

In all, the design of your codebase is up to you. Let testing help you make these decisions. Refactoring is much easier because any change you make should still pass the tests you've already written. For clarity, we'll keep isNumber public for now.

Updating Functions

We haven't updated the add() method yet, which is the ultimate goal. Remember, we want to make sure both parameters entered are numbers before trying to add. To start, let's make sure .add() returns false if a non-number is passed into it. Here's our test block:

QUnit.test('Checking the `add` method in Calcs', function() {
    // ... previous tests ... 
    equal(Calcs.add('foo', 2), false, 'When a non-number passed in the first param, the function will return false.'); 
    equal(Calcs.add(2, 'bar'), false, 'When a non-number is passed in the second param, the function will return false.');
    equal(Calcs.add('foo', 'bar'), false, 'When two non-numbers are passed, the function will return false.');
});

All of these tests may seem redundant, but we want to try and cover each scenario of a non-numer entering our function. Again, writing tests first makes you focus on updating functions to pass. Let's make a change to the add() method which will fulfill that function. Here's our updated method:

const add = function(a ,b) {
    if(isNumber(a) && isNumber(b)) {
        return a + b
    } else {
        return false
    }
}

Refactoring

At this point, we have all of our tests passing and our application will function as intended. You can now go back and refactor knowing that your tests will fail if you break a function somewhere.

Summary

  • Writing tests first helps you solve one - and only one - problem at a time.
  • Passing the test is more important (at first) than writing clever code. Once your test passes, you can go back and refactor with confidence.
  • All new functions (or changes to existing functions) get their own, explicit test.
  • Write multiple tests covering all possible scenarios for failure to make sure you are writing robust and maintainable code.
  • Apps Script does not have a build step, so be careful about adding private functions that are difficult to test.

Unit Testing GAS Part 2: Simple Tests

If you're brand new to unit testing, start with the first post in this series to get caught up.

Part 1: QUnit Setup
Part 2: Simple Tests
Part 3: Adding and Updating Functions
Part 4: Error Handling
Part 5: Testing Objects and Arrays


Simple Tests

From part one, unit tests are for single units of code. They test a specific function for a specific result. I found a helpful living guide on writing unit tests that included some very clear expectations:

Unit tests are isolated and independent of each other. Any given behaviour should be specified in one and only one test. The execution/order of execution of one test cannot affect the others.

Let's create a simple class with some properties and methods we can test. We'll use QUnit to write some tests for those methods. Once we've covered the basics, a future post will look at more complex application structures and tests.

Source

The completed source for this part can be found here.

Writing Functions and Tests

Let's start by defining a Calculations class using Bruce Mcpherson's recommended namespacing structure to keep everything neat. If you're following along, create a Script file named calculations.gs in your editor and add the following code.

const Calcs = (function() {
    const name = 'Calculation class';

    const about = function() {
        return 'A class for calculating things';
    }

    return {
        name: name,
        about: about,
    }
})();

A Note on Naming Tests

Following the testing guide, naming tests clearly is important as their messages will be your guides to problem solving. Each test is given a specific message parameter that has a specific action...should...result format. An named action (calling a class parameter or method) should do something and end in a defined result.

In QUnit for GAS, the result is defined as the expected result in assertions that accept that paramter (keep reading below).

Writing Simple Tests

Now it's time to define some tests. The biggest change in my thinking came when I switched to writing tests first to define what I want the outcome to be before diving in and figuring out if my function is giving me the right output or not. Create a new script file called tests.gs and add the following:

function calcTests() {
    QUnit.test('Checking the Calcs class parameters', function() {
        ok(Calcs.name, 'The name parameter should be available in the namespace.');
        equal(Calcs.about(), 'A class of calculation methods', 'The about method should return the Calcs class description.');
        ok(Calcs.author(), 'The author method should return the Calcs class author description.');
    });
}

Breaking this block down:

  • function calcTests() { ... }: a wrapper which contains several tests. The name is arbitrary, but it should describe what you're testing in general.
  • QUnit.test(name, callback): a method requiring two parameters: a name and a callback function. The callback defines specific assertions (or tests) to run.

Inside the test are the specific assertions we're making about the function:

  • ok(state, [message]): The simplest test that evaluates the truthy/falsy state of the input. The message parameter is optional.
  • equal/notEqual(expected, actual, [message]): Comparisons of expected values with actual returned along with an optional message.

Naming and writing good messaging takes practice and I'm still working on a system that works well for me. The great thing is that if a system isn't working well, just rename it or change the messaging!

The last step before we can run tests is to tell QUnit where to look for those tests in the config file we defined in part one. Open your config.gs file and make sure it looks like this (excluding comments):

QUnit.helpers( this );

// Define the tests to run. Each function is a collection of tests.
function tests() {
  console = Logger; // Match JS
  calcTests();   // Our new tests defined in tests.gs
}

// runs inside a web app, results displayed in HTML.
function doGet( e ) {
  QUnit.urlParams( e.parameter );
  QUnit.config({
    title: "QUnit for GAS" // Sets the title of the test page.
  });

  // Pass the tests() wrapper function with our defined
  // tests into QUnit for testing
  QUnit.load( tests );

  // Return the web app HTML
  return QUnit.getHtml();
};

What's happening:

  • calcTests(), our tests function, is included in the tests() wrapper function in the QUnit config (line 8).
  • tests() is loaded into QUnit with QUnit.load(tests) (line 20)

Running Tests

QUnit is run as a web application through apps script. Go to Publish and choose Deploy as web app.... In the popup, set the new version and limit access to yourself.

You'll need to verify the application can have access to your account. Once that is done, you can open your web application link. If you've done your setup correctly, you should see your three test results:

Test results from the QUnit web app.

You just ran your first unit tests!

Failing a Test

There are plenty of ways to write failing tests. They fail either because your code doesn't produce the expected value or because your test is expecting something that isn't happening. Let's make a small change to our Calcs class which will cause a test to fail.

In the class, change the .about method to:

const about = function() {
    return 'A class of calculation method';
  }

Since our test is asserting that this function will return the string, A class of calculation methods, we can expect this test to fail because it will evaluate to false. Run your tests again either by reloading the web app page. Sure enough, we have a failure:

A failed test in QUnit

There are a couple things to note from this result:

  1. The expected result is defined in your test function.
  2. The actual result and the difference are shown so you can identify the point of failure (and yes, your tests can be the point of failure!)

Since the .about() method fails its test, I know I need to go back and fix the bug. Adding an 's' to 'method' solves the bug. Reloading the page will confirm with a passed test.

Stack traces in QUnit for GAS are marginally helpful. This is because the testing happens on Googles servers, not your computer, so there are several steps in the tooling that add layers of trace data. Some ways to make this more readable are to add code references to your tests file or to have function-based naming so you can find what failed. For this example, we don't have to worry too much, but we'll look into more complex applications at a later point.

Changing Your Code

The whole point of unit testing is that you catch breaking changes before your code is released. Let's make a change to our Calcs class and write a test to make sure that nothing is broken. Start by writing a simple test to define what we want that function to do.

// tests.gs
QUnit.test('About Calcs test', function() {
    ...
    ok(Calcs.author(), 'The author method is publicly available');
    ...
})

...and then add the function to Calcs which will pass the test.

// calculations.gs
const Calcs = (function() {
    // ...
    const author = function() {
        return 'This ' + name + 'is authored by Brian.'
    }
    ...
})

Reload your web app page. What happens?

Your test should have failed (if you followed my code above) with the error, Cannot find function author in object [object Object]. But why?

Something is wrong...the test couldn't find the function author() even though I added it to my class. The explanation is that I never exported that function in the return statement! Since it wasn't exported, the test fails. A potential bug in my application has been caught early and is simple to diagnose and repair before it causes user errors later. Update the return statement in the calculations class to:

// calculations.gs
...
return {
    name: name,
    about: about,
    author: author,
}
...

...and run the tests again by reloading the web app to see that everything now passes.

Summary

This is the first glimpse into using QUnit inside an Apps Script project. Once the setup is complete, you can start writing tests for what you expect your code to do, which gives you clarity and insight into actually writing the function while knowing your test will catch bugs.

  • Tests are grouped into wrapper functions, usually by similarity in purpose.
  • Specific tests are run with the QUnit.test() method which takes two parameters:
    1. A title for the tests
    2. A callback function defining each type of test
  • Tests are passed into a tests() wrapper function in the config file.
  • The tests() wrapper is passed into QUnit.load() to run in a web app.
  • ok, equal, and notEqual are simple checks for true/false results when the expected and actual results are compared.