8
\$\begingroup\$

In an effort to make my code more testable, I'm trying to make use of constructors and prototypes.

Here is a working Google Apps Script (GAS) for creating a new submenu under the Addon main menu in a Google Spreadsheet. If a user clicks the 'Show' menu item, a sidebar will appear.

var UI = function (menuName, menuFunction, sidebarFile, sidebarTitle) {
    this.menuName = menuName;
    this.menuFunction = menuFunction;
    this.sidebarFile = sidebarFile;
    this.sidebarTitle = sidebarTitle;
};

UI.prototype.createAddonMenu = function () {
    try {
        SpreadsheetApp.getUi()
            .createAddonMenu()
            .addItem(this.menuName, this.menuFunction)
            .addToUi();
        Logger.log('onOpen (success): building menu');

    } catch (e) {
        Logger.log('onOpen (fail): ' +  e);
    }
};

UI.prototype.showSidebar = function () {
    try {
        var ui = HtmlService.createTemplateFromFile(this.sidebarFile)
            .evaluate()
            .setTitle(this.sidebarTitle)
            .setSandboxMode(HtmlService.SandboxMode.IFRAME);

        SpreadsheetApp.getUi()
            .showSidebar(ui);
        Logger.log('showSidebar (success): showing sidebar');

    } catch (e) {
        Logger.log('showSidebar (fail): ' + e);
    }
};

var ui = new UI('Show', 'showSidebar', 'index', 'Awesome Title');

// onOpen is a built-in Google Apps Script function that gets called when the spreadsheet is opened
function onOpen(e) {
    return ui.createAddonMenu();
}

// showSidebar is a standalone function and returns the ui showSidebar method. needs to be this way in order to be called from createAddonMenu
function showSidebar() {
    return ui.showSidebar();
}

Questions:

  1. I'd like to be able to write unit tests for both createAddonMenu and showSidebar, but at this moment I don't believe it's possible as I'm not returning a value from either method; I'm simply just calling Google's third party code for creating a menu and showing a sidebar. Would I need to create a wrapper around any code calling a third party's code, and then call that within my method? I know I'm not supposed to test third party code, so how would I go about testing to make sure it gets called and doesn't return an error?

  2. I'd like a sanity check on when I should create a constructor. I feel like maybe I'm trying to shove too many arguments into it and maybe trying to do too much. Should I create two separate constructors (one for createMenu and one for showSidebar)? Should I even be using constructors at all?

  3. Finally, any other feedback?

\$\endgroup\$
2
  • \$\begingroup\$ Forgive me if this is obvious, but how do you make this work? You open google sheets and throw it in tools - script editor and then? Run? Do you need a trigger on the sheet? \$\endgroup\$ Jan 27, 2016 at 14:54
  • 1
    \$\begingroup\$ @Raystafarian, here is the spreadsheet. Just by opening the spreadsheet will cause the script to run. You will be able to go to the Add-Ons menu > showSidebar > Show, which will open up the sidebar. \$\endgroup\$
    – Blexy
    Jan 27, 2016 at 18:47

2 Answers 2

2
\$\begingroup\$

1. Testing:

Although your functions don't return a value and they catch any error that is thrown, the functions do log whether the call was successful or not. You could use the Logger to test if they fail like so:

Logger.clear();
ui.showSidebar();
var string = Logger.getLog();
if (string.indeOf("(fail)") === -1) throw "test for 'showSidebar' failed";

Note: If your unit tests are run in the script editor some of the third party functions could throw errors that wouldn't occur when called from the spreadsheet. The ones in your current functions are all fine last I checked, but function calls like SpreadsheetApp.getActive() cannot be made from the script editor.

Writing wrapper classes would be the safest way to ensure that your functions are correct, but that assumes that your wrappers and mocks are perfectly written. You have to consider whether the work it would take to write/debug/test the wrapper and mock classes is worth the value of having unit tests for your functions--especially since there are currently only two and they are rather small.


2. OOP in Google Apps Script

Where to call your constructor

Declaring variables in the global scope is dangerous since Google doesn't offer specifics on the order that it's interpreter/compiler (who knows which) reads the code. Basically things like:

var UI = function () {}

and

UI.prototype.showSidebar = function () {};

and

var ui = new UI();

aren't guaranteed to be executed in the intended order, specifically if they're in separate script files. If you wish to dabble in this dark art (who doesn't?), please note the following:

  • Functions are hoisted
  • The rest of the code appears to be executed in the order that your script files were created

I would recommend making one of your script files a sort of "main" file; make this file the only place you preform logic on the global scope to ensure that all the global logic is executed in the intended order.

Note: Global logic is executed every time you execute a script.

Constructor and Prototype in Google Apps Scripts

The way your code creates the UI class depends on global logic. If you declare a UI instance at the global scope in a separate script file, the UI class or it's prototype might not yet be defined.

A way around this would be to use a factory function instead:

function createUI() {
    return {
        createAddonMenu : function () {},
        showSidebar : function () {}

    };
}

The factory function will be hoisted and all is well in the global scope.

To many parameters

Consider passing a single object for your constructor's parameter:

function createUI(settings) {
    var menuName = settings.menu.name;
    var menuFunction = settings.menu.action;
    // ...
}
var ui = createUI({
    menu : {
        name : "Show",
        action : "showSidebar"
    }
    sidebar : {
        // ...
    }
});

This gives names to the parameters at the caller's end, it makes it so the order of the parameters is irrelevant, and it makes optional values easier to work with since the order of other parameters won't be affected if one is omitted. When using this technique you may want to validate that all required parameters exist inside the parameter object.

\$\endgroup\$
-1
\$\begingroup\$

There's no real need to wrap these functions. Using Object-Oriented Programming in this case would mean to me that you are expecting to potentially create multiple instances of the UI to use else where. Instead, there's only going to be 1 custom menu be created.

The sample code provided by Google in the script editor under

Help menu >> Welcome Screen >> Google Sheets Add-on

doesn't use constructors and nowhere in official docs have I ever seen OO programming used in snippets. Other users certainly use that pattern for larger programs though.

Instead of UI wouldn't Menu be a better class name anyhow? UI is what pops up in the sidebar or modal. Now if your Add-on has the potential to serve up lots of different UI views, that may trigger the need to organize that piece in an OO manner later.

In an effort to make my code more testable

Was there a specific issue with testing you ran into with basic functional programming that triggered the need to convert to OO? Code reuse in a large code base is typically the argument instead of testing.

how would I go about testing to make sure it gets called

You're already using Logger.log('showSidebar (success): showing sidebar');, what is wrong with this? Manually clicking the Show menu item or manually running the showSidebar() function from within the editor will run that line.

Unless your personal preference is OO or that's the background you're coming from, this particular section of code doesn't need it, in my opinion. Functional Programming is more clear and concise for small tasks.

Disclaimer: I don't have much OO experience, but have been working with Apps Script exclusively for over 4 years via functional programming

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I agree with just about everything in your answer and don't think your answer score at all reflects your expertise. \$\endgroup\$ Apr 29, 2016 at 19:17
  • 1
    \$\begingroup\$ +1 No idea why you are downvoted so much. OO in this case is overkill and does not provide any significant benefits in this scenario. \$\endgroup\$ Mar 11, 2018 at 23:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.