Why It’s Crucial
Unit testing is an extremely important practice. It helps developers avoid bugs easily and lets you know when something is broken immediately. This makes refactoring easier. There is a great overview regarding the reasons why it’s a must in a recent article by Tyler Hawkins. Inexperienced developers tend to rush into coding and more often than not end up chasing bugs for hours or days before they get things to work again. If you are unfamiliar with unit testing, please feel free to pause right here, read up, and come back. I’ll wait, I promise! 😎
For that reason, I built my own library with Google Apps Script in mind. You are welcome to check it out at this GitHub repository.
So, I began to wonder what I would need from such a library. This is my checklist:
- Tests should run in both my local environment and in the GAS environment (yes, testing GAS locally is not as crazy as it sounds).
- It must be lightweight.
- It must be easy to maintain.
Let’s see what’s inside the library. There is a small class called
UnitTestingApp with just a few simple functions. After all, “lightweight and easy to maintain,” remember? There is also the
MockData class that allows you to add and work with, well, mock data, which is especially important when running tests offline. There will be more on that later.
Enabling, Disabling, and Checking the Status of the Tests
The two methods,
disable(), and the
isEnabled property, are hopefully self-explanatory. The syntax is:
Choosing the Environment with runInGas(Boolean)
runInGas(Boolean)function allows developers to choose the environment in which we want the tests that follow to run, as follows:
Then, we have the actual built-in testing methods,
assert() is the main method of the class. The first argument that it takes is the condition, and it checks whether the condition is truthy or falsy. The condition can either be a boolean value or a function that returns such a condition, with the function being the preferred approach for error-catching reasons. If the condition is truthy, it logs out a “PASSED” message. Otherwise, it logs out a “FAILED” message. If you pass in a function that throws an error, the method will catch the error and log out a “FAILED” message. For example:
catchErr(callback, expectedErrorMessage, message)
The goal of this method is to test whether your callback function (
callback) catches the correct error. What you want to do is make sure that the callback actually throws an error. Then, the
catchErr()method will check if it’s the correct one. Finally, it will log out the corresponding message. For example, let’s say you have a function that returns the square value of a number you pass as the argument, and you want to throw an error if the argument isn’t a number. Then, you can test the error this way:
This method runs a test to check whether the argument is a 2D array. This is useful for testing spreadsheet values before inserting them into a spreadsheet:
Then, there are a couple of helper methods, which include
This helps with readability by printing a header in the console like this:
This is a straightforward method that clears the console log if you are in the local environment.
Finally, there is a way to add new tests to the calls with the
Let’s say, for example, that we need a test that would check whether a number is even:
We will not be using this method in our example, but it’s good to know that it exists and that this library is extensible.
Thinking the Project Through
The app we will be building is a simple one. It will pull calendar data for the coming two weeks, save it in a spreadsheet, convert it into an HTML table, and send it by email.
So, what are the classes and corresponding tests that we are going to need? Let’s break them down:
We will need an
Eventsclass that will take two arguments: the start date and end date. Then, it will connect to the
CalendarAppclass and retrieve all scheduled events between these two dates. This specifically includes the following fields: start time, end time, title, location, guest list, and our status (i.e., if we confirmed our participation or not).
We will need the following tests for the
- whether it returns a 2D array
- whether it throws an error when one of the arguments isn’t a date
Once we validate that the class works as expected, we will need to convert its output to HTML code. For this purpose, we will use a class called
ArrayToHtml, which will take our 2D array as input and return an HTML table. Thereafter, we will need a test to validate that this class returns a valid HTML table.
Setting Up the Local Environment
For local GAS development, VS Code is the best IDE, as it supports autocomplete. For autocomplete to work, make sure you have node.js and npm installed. Then, you will want to install the following package by running this command in your terminal:
npm install --save @types/google-apps-script
After that, create a local
.jsfile and type in the following:
import 'google-apps-script';. You will later want to add that file to
.claspignoreso that it doesn’t get pushed to your project online.
Another package you want to install is
clasp. To install it, run the following command in your terminal:
npm install @google/clasp -g
Now you can use the command line to sync your code to the server. The way I normally work is:
- I create a Google Apps Script project online.
- I copy its ID.
- Then, I create a local folder for my project and navigate there in my terminal.
- If I’m not logged in, or if I’m logged in with a different account, I run
- Then, I run
clasp clone “<PROJECT_ID>”, which sets up the sync between my actual project and my local folder.
- Then, I run
clasp push -w, which syncs my code to the server every time I save a local file.
- To stop pushing code online when I’m done, I type
Note that local files must have the
.jsextension; they will then automatically be changed to the Apps Script’s native
.gs. Feel free to read up more on clasp here: https://developers.google.com/apps-script/guides/clasp.
I also highly recommend installing
npm install -g nodemon. This way, you can have your tests execute automatically every time you save your tests file.
Setting Up the Tests Environment
It is a best practice to write your tests before you write the code. You will have your tests showing the
FAILEDresults, and as you progress through your code, they will turn to
PASSEDone by one. When they all pass, because you thought your tests through thoroughly, including the edge cases, you will know that your code is stable.
If you have
gitinstalled, do a clone of my repository by running
If you do not have git installed, even though I suggest that you do this later, for now you can just navigate to the GitHub repository and copy, paste, and save the
Copy the three files to your project folder. Feel free to rename the
TestingTemplate.jsfile to something like
Now, let’s have a look at the testing template. This is where all the magic is going to happen:
Let’s look at what’s happening here to make sure everything is clear.
In the first lines, we’re requiring the
if (typeof require !== 'undefined')statement makes sure that we’re only running
requiredoesn’t exist in Apps Script, so the
ifstatement makes sure it doesn’t throw an error.
Then, we have the
runTests()function with all of our testing code.
enable()the test, and
clearConsole() to clear the results of all previous tests. Then, we have two blocks of offline and online tests set up with
printHeader()allows us to see precisely the environment in which we are running.
The IIFE in the end of the file checks whether or not we are in the Apps Script environment. If not, it executes the
runTests()function. When in the GAS online IDE,
runTests()needs to be executed manually.
Now, all we need to do is write our code below the “offline tests” and “online tests” headers respectively.
Writing the Tests
Above, we have already determined the tests that we need to write. Let’s now actually write them and put them in the offline tests block for now:
The first two methods are
catchErr(), which create new
Eventsobjects. Each passes a string argument instead of a date. We want our
Eventsclass to throw an error when this happens.
Then, we want to check that our
get()method returns a 2D array, so we feed it into the
Finally, we test that we have at least a few events to work with by checking the length of the 2D array with
Now, in the terminal navigate to your project folder and run
node Tests.js if you haven’t installed
nodemon). At this stage, all of your tests should be failing like so:
Creating the Events Class
Now we know that we want to be able to initiate an
new Events(startDate, endDate)and have a
get()method retrieve the data as a 2D array. It is also a best practice to add a
print()method to your classes so that you can log out its contents easily. We will need this method later on.
Let’s start building it out.
Eventsclass needs to go into a file of its own.
Let’s start with our edge cases. The constructor takes two arguments:
endTimeWe want to check that
endTimeare instances of
Dateand throw an error in case they are not. We’ve already defined the error messages in our test file, so let’s use them here.
You also need to export the module with
module.exportsto make sure that the test file can import it. Just like with
require, we need to validate the
modulesobject with an
ifstatement to make sure that we don’t call it in Google Apps Script and throw an error. Save the file and
requireit in the
Tests.jsfile like so:
Save the tests file. If you ran
nodemon Tests.js, your tests should now be showing this:
Congratulations! The first two tests are now passing!
Pulling the Calendar Data
Now that we need to pull the calendar data, we need to execute the tests in online mode. Let’s continue writing our class.
You will notice that I am
returning to interrupt the function if the type of
undefinedmeaning that we are in the local environment. This prevents an error from being thrown. It’s a temporary measure, and we will implement a more elegant solution later on.
We have a private
_eventsproperty, implemented with a
WeakMapwhich holds the 2D array. We initiate it with the
headers. Then, we call
CalendarApp, get a list of events, pull the necessary data from each event, and put it line by line (i.e., array by array) into the
get(row) returns either all of the data in the `_events` property, or just a row.
print(row)similarly either prints everything or a given row.
Now it’s time to add the following test to the online part:
test.is2dArray(events.get(), 'Calendar Data is a 2D array');
If you haven’t already done so, it’s time to create your project online and
clasp pushyour code there. Once this is done, navigate to the IDE and manually execute the
runTests()function. You will get the following result:
Great, we’re getting a 2D array.
Does this mean that from this point forward we are going to have to run all of our tests online because we can’t read calendar data locally? No, there is a workaround; we will use mock data. Let’s see how this is done.
Using Mock Data for Local Testing
Our calendar data has the following format:
First of all, let’s use
events.print()to log out this content and copy it to your buffer. Now, let’s place it inside of our
EventsIIFE before the
Eventsclass like this:
We then wrap our data inside
JSON.parse()to turn it back into an object. Then, we initiate the
MockDataclass if we are in offline mode (tested with
CalendarApp === 'undefined'). Let’s talk more about the
MockDataallows you to register, delete, and retrieve any kind of data with a key of type string. We use
mockData.addData(key, data) to add data,
mockData.getData(key) to retrieve data, and
mockData.deleteData(key) to delete data. The class uses the Singleton design pattern; therefore, it’s available anywhere in the application. This data can be shared easily.
So, we will add our mock data if we are offline, and then we can simply add this data to our
_events property without actually calling
CalendarApp. This will allow us to continue running our tests offline.
Our next task is to convert our 2D array to an HTML array. For that, we will build a new class, but let’s again start with the tests.
2D Array to HTML Conversion
We’re now back in offline mode.
ArrayToHtml class will be very simple. It will only have the constructor function that will take the array. Let’s start with our test:
This test is a little simplistic. You may wish to write another one that will also validate that the table has at least one
<tr> and one
<td> element. This would be a good time to use the
test.addNewTest() method, but I will leave it up to you as a challenge. So, now create the
ArrayToHtml.js file with an empty class,
export it as a module, and
require it in your
Tests.js file. Your test should now be failing, so let’s build it out.
When you save this, your tests should now be passing.
You may replicate the same tests we have offline to online if you want, but strictly speaking, it is not necessary, as we’ve already tested the necessary behavior offline. However, you may wish to do so in order to be able to run all tests online in the future.
Building the App
I find that knowing for certain that you have working classes before you begin building the app is of great comfort. In the testing file, you may now change `test.enable()` to `test.disable()` and create your app file. Depending on your needs, the main function of your app may look something like this:
See how easy this was now that we’ve built our backbone?
We now have a very organized app with stable classes that we’ve thoroughly tested. And when you need to add more code to them, just switch the tests back on and make sure old tests keep working. This will ensure that your code will stay backwards-compatible and not break anything anywhere.
If you found this useful, I’d appreciate a clap and/or a comment, you feedback goes a long way in helping me deliver quality content.
About the Author
Dmitry Kostyuk is a full-time GAS developer and Founder of Wurkspaces.dev.