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


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.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.