Unit Testing in GAS Part 4: Error Handling

Published: 2019-12-04 11:39 |

Category: Code | Tags: gas qunit, google apps script, qunit, testing, tutorial


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


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 <https://api.qunitjs.com/assert/throws>`__ 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.

Comments are always open. You can get in touch by sending me an email at brian@ohheybrian.com