Giter Site home page Giter Site logo

utgs's Introduction

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.

utgs's People

Contributors

brainysmurf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

utgs's Issues

Improve tests for throwing errors

Currently the api only allows to test that a function throws an error if a particular type.

Due to limitations in AppsScripts, the test can fail even though an error is actually thrown, since the isinstance test fails. An error objected created in the utgs library won't be an instance of an error object in the main project.

Also, error messages should also be exposed via the api, so a test can be do based on the message.

`assert.throwsError` not recognizing thrown error

When testing that a constructor throws an error, the test fails with the message, No error was thrown, expecting error of type 'Error'. Checking err instanceof Error returns true in the console during function execution.

class MyClass {
  constructor(url) {
    let notAllowed = "api/v1";

    if(url.includes(notAllowed)) {
      let error = new Error('You can't do that');
      console.log(error instanceof Error) // => true
      throw error;
    }
  }
}

// Throws an Error in the console
function simple_test() {
  new MyClass("https://www.example.com/api/v1")
}


// Fails the unit test with "No error was thrown, expecting error of type 'Error'"
function utgsTest() {
  const { describe, it, assert } = Utgs.module()
  describe('A unit test', function() {
    it('will fail with a bad structure', function() {
      assert.throwsError("api/v1 in the URL", function() {
        new MyClass("https://www.example.com/api/v1")
      })
    })
  })
} 

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.