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 GAS Part 1: QUnit Setup

Unit Testing GAS Part 1: QUnit Setup thumbnail

I'm not good at writing testable code. I'm more of a 'figure it out when it breaks' kind of hobby programmer. The problem with this is that I am constantly making my own bugs and not really finding them until a bad time.

Unit testing is the process of running automated tests against your code to make sure it's working correctly. Each test is for one unit of code - a single function, usually. It expects a value and will pass or fail based on the value received as part of the test.

To get better, I forced myself to write unit tests in Google Apps Script for two reasons:

  1. I've been writing a lot of Apps Script code lately,
  2. There are not many good methods for unit testing in GAS.

This series

The point of this series is to force myself to learn, and use, a unit testing method when writing code and to update the far outdataed unit testing tutorials for Apps Script published online already. I've tried several testing libraries but will be using QUnit as the testing suite.

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

I'm following Miguel Grinberg's method of posting tutorial code as tagged versions of a GitHub project. Each post will link to a specific tag with the completed source code for that section.

Here's the source for this post

Now, for large projects, you could argue that using clasp and a traditional unit testing library like Mocha or Jasmine is preferable, and you might be right. But, for the purposes of learning, I wanted to keep everything as 'pure' as I could, so all files and tests are written and tests in the online apps script editor.

What is QUint?

It's a testing framework developed and maintained by the jQuery Foundation. It is used in jQuery development to make sure things don't self destruct as the library expands.

QUnit is written for Javascript. Because GAS is based on Javascript, there is a handy library which can be installed in your apps script project.

When testing on your local computer, tests are run by your machine. With Apps Script, everything is run on Google's servers. The QUnit library exposes the framework through a web app that fetches the framework code and executes it when the web app loads.

Install

You can install QUnit for apps script by going to Resources > Libraries in the editor and searching for MxL38OxqIK-B73jyDTvCe-OBao7QLBR4j in the key field. Select v4 and save. Now the QUnit object is available in your project.

Setup

The QUnit library needs some configuration to work with an apps script project. There are three parts to the setup: 1) Declaring QUnit at the global scope, 2) defining tests, and 3) configuring the web application to run the tests.

1. Instantiate QUnit

Once the library is loaded, it needs to be instantiated at the global level to run. Create a new script file called config.gs to hold all of your QUnit code.

The first line should be:

QUnit.helpers(this);

This exposes all assertion methods in the QUnit library (ok, notEqual, expect, etc.) instead of a pared-down object.

2. Define Tests

Tests are defined within wrapper functions that can be passed into QUnit. This tests function will simply hold a list of tests to run when the web application is loaded. We won't be writing any tests in this post but go ahead and add a wrapper for populate later.

function tests() {
    console = Logger; // Match JS
    // Test definitions will be added here
}

3. Web App Config

TheQUnit.config() object declares settings for the web app, so it gets wrapped in the doGet() function. URL params are used to pass information from the app to the testing library with QUnit.urlParams().

QUnit also has a config object which can set default behaviors. You can see a full config object in the project source. For this simple setup, all I'm going to declare is the web app title. Add this to your config.gs file:

function doGet( e ) {
    QUnit.urlParams( e.parameter );
    QUnit.config({
        title: "QUnit for GAS",
    });
    return QUnit.getHtml();
};

Now you're ready to write some code. Running QUnit right now won't do anything; that will come in part 2.

Summary

  • QUnit is a testing library developed by the jQuery foundation.
  • Google Apps Script is Javascript-like, so a JS testing library can be modified to test Apps Script projects.
  • QUnit for Google Apps Script is a library which can be used in the online Apps Script editor.
  • It runs with a web app and is defined by a doGet method and a config object.

The featured photo is 'Bricklayer' by AstridWestvang is licensed under CC BY-NC-ND

I was trying to come up with a metaphor for construction without tests, and this seemed to be the closest I could get.