npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@classroomtechtools/unittesting

v2.0.3

Published

Exports describe and it functions for unit testing purposes

Downloads

6

Readme

Utgs

Unit Testing with Google appsScripts

A library that can be used with the online editor to conduct unit tests. You create a function such as Tests and declare how your code is supposed to work. Whenever you make changes to your code, you just run the test function to check.

Documentation on usage is available.

Quickstart

Use it as a library:

  • The Script id is 1cSXAtmjHA61q0KXQLo4nVq7M2ISdhIq01qucWxbpPqZGYldoxE-hxF2R.
  • The default identifier is Utgs
function Tests () {
  // sorta like importing, this inits the variables
  const {describe, it, assert} = Utgs.module(); 

  describe("Test Category 1", function () {
    it("Have the value of Yes", function () {
      assert.equals({
        comment: 'If it fails, it displays in the log',
        expected: 'Yes',
        actual: 'Yes'
      });
    });
    it("Have the value of No", function () {
      assert.equals({
        comment: 'If it fails, it displays in the log',
        expected: 'No',
        actual: 'Yes'
      });
    });
  });
}

If value is not "Yes", it throws an error that lets you track it down:

Test Category 1
  ✔ Have the value of Yes
  ✘ Have the value of No -> Error: Comment: If it fails, it displays in the log
   ---> Error message: Expected <"No"> (String) but was <"Yes"> (String)

But when everything works, bask in the glory of lots of glorious check marks.

Examples

Use objectEquals to test that two different objects have the same keys and values on the keys. This test will fail:

function Tests () {
  // sorta like importing, this inits the variables
  const {describe, it, assert} = Utgs.module(); 
  describe("Test Object", function () {
    it("objects are the same", function () {
      assert.objectEquals({
        comment: 'If it fails, it displays in the log',
        expected: {yes: "yes", no: "no"},
        actual: {ha: "ha", yes: "yes"}
      });
    });
  });
}
Test Object
  ✘ objects are the same -> Error: Comment: If it fails, it displays in the log
   ---> Error message: Expected keys no, yes but found ha, yes

Use arrayEquals to test that two different arrays are the same:

function Tests () {
  // sorta like importing, this inits the variables
  const {describe, it, assert} = module(); 
  describe("Test Object", function () {
    it("objects are the same", function () {
      assert.arrayEquals({
        comment: 'If it fails, it displays in the log',
        expected: [1, 2, 3],
        actual: [1, 2]
      });
    });
  });
}
Test Object
  ✘ objects are the same -> Error: Comment: If it fails, it displays in the log
   ---> Error message: Expected keys 0, 1, 2 but found 0, 1
    

The documentation provides all the other assertions available on the assert object.

Motivation

Unit testing lets us achieve assurance that our code is doing exactly what we think it's doing. In addition, the author wanted a way to write unit tests without having to rely on the built-in objects such as SpreadsheetApp.

Dependency Injection

The above unit test example test that the function returns some value. But much of code written with appscripts is really that simple.

This section will illustrate how to also use it to implement unit tests that are more realistic, ones that have side effects, such as writing to a spreadsheet. These are known as side effects, as a function's result is not limited to a return value, but an effect that is produced by something it uses, a dependency.

Specifically, how do we test API calls with Spreadsheet service, where it doesn't give us back anything but does have side effects that is producing the very thing we need to test for. How do we test a function that writes to a spreadsheet actually does write to the spreadsheet?

There is a way, using a coding pattern called dependency injection. It's an appoach to writing functions where you declare your dependencies at the end of the function signature in an object with default values. Here's a simple example, where we're using the Math object to write a floor function:

function floor (num, {dependency=Math}={}) {
  return dependency.floor(num);
}
floor(1); // uses Math since it's the default value for dependency variable

The above example is not very useful, but illustrates the mechanism. Instead of the function body using the name of the dependency, we've wired it so that it's using a variable dependency whose default value, if it is called as normal without defining the last object specifically, will be Math. That gives us a function where we can change the dependency it actually uses … and in this case completely changes what it does:

const MathLike = {floor: (num) => num + 'floor'};
floor(1, {depedency: MathLike});  // "1floor"

We can use this approach to test for side effects.

Dependency Injection in action

Let's say we want to write a function that writes some values in a spreadsheet to x row. So for example writeRow('Sheet1', 3, ['a', 'b', 'c']) will make row 3 have [1, 2, 3]in the sheet Sheet1.

/**
 * Write a spreadsheet row, starting from column A to however many values are contained in values param
 * @param {String} sheet - The sheet name
 * @param {Number} row - the row number which will take the values 
 * @param {Any[]} values - an array that represents the rows
 */
function writeRow (sheet, row, values) {
  const sheet = SpreadsheetApp.getSheetByName(sheet);
  const range = sheet.getRange(`A${row}:${row}`);
  range.setValues([values])
}

So how do we test it? Shouldn't we test that it actually writes to the spreadsheet as expected? So let it execute the setValues, then read it back in, and check you got the expected result. The author suggests we do not. We just need to test that the call to setValue has some side effect, but not the default side effect of actually writing to a spreadsheet. That's where dependency injection comes in.

Let's rewrite our function that incorporates dependency injection, which is just a way of providing some parameters at the end of the function signature. We have to rewrite the body, though, so that we use Spreadsheet_App instead of the direct SpreadsheetApp, but if this last parameter is not passed, the former will actually be the latter.

function writeRow (sheet, row, values, {Spreadsheet_App=SpreadsheetApp}={}) {
  const sheet = Spreadsheet_App.getActiveSpreadsheet().getSheetByName(sheet);
  const range = sheet.getRange(`A${row}:${row}`);
  range.setValues([values])
}

This is a function we can test for the side effect directly.

The idea of how to use it can be represented here. When we call writeRow, we hand an object that we mock ss which will be the value of Spreadsheet_App of the call.

function Test() {
  const {description, it, assert} = Utgs.module()
  const ss = SS();  // SS is defined below
  const values = ['a', 'b', 'c'];
  writeRow('any', 3, values, {Spreadsheet_App: ss})
  const actual = ss.state.arr;
  const expected = [values];
  assert.arrayEquals({actual, expected, comment: 'convert to 2d array'});
}

We just need some code that makes const ss = SS() work so that we can do ss.state.arr later and look at what was passed to setValues.

// setup the mock for dependency injection
// (these arrow functions without return statements are returning objects,
//   hence the perhaps odd-looking syntax)
const SS = () => {
  const state = {};
  return {
    getSheetByName: () => ({
      getRange: () => ({
        setValues: (arr) => {
          state.arr = [arr];
        }
      })
    })
  };
};

That way, our test works as expected.

Pitfalls

Eagle-eye appscripters might have balked at the code above in the dependency injection example. In fact, the test we created will pass, but when used in a real-life situation, will not work as expected, and will fail with the error:

The number of columns in the data does not match the number of columns in the range. The data has 3 but the range has 26.

The whole point of testing is to avoid such things! We need to compensate. Let's look into it.

The error is because setValues has the behaviour that the specified range must match the values that were passed to it. But we did this:

SpreadsheetApp.getSheetByName(sheet).getRange(`A${row}:${row}`).setValues([values])

Notice the getRange function is passed the value of A3:3 which specifies the whole row, rather than just three columns across as it should.

The fix is fairly easy, we just have to rewrite writeRow using the different getRange with four values passed to it instead of one:

function writeRow (sheet, row, values, {Spreadsheet_App=SpreadsheetApp}={}) {
  const sheet = Spreadsheet_App.getActiveSpreadsheet().getSheetByName(sheet)
  const range = sheet.getRange(row, 1, 1, values.length);
  range.setValues([values])
}

As we can see, even though we avoid having to use Google's Spreadsheet service in our testing, you still need to pay attention to how they actually work.

Node

Can also be installed via node, npm install @classroomtechtools/unittesting.

This package has unit tests on itself, which is also useful to check out how to use it.

npm run test

Thanks

Much of the original code came from GSUnit, with additional refactoring and the additional function assertions.