<

I want to test with Google Apps Script too! (Clasp + Typescript + Jest)

I have published a library in Google Apps Script (hereafter, GAS). When developing the library, I chose a tech stack of Clasp + Typescript + Jest to shorten the test feedback cycle. I would like to share my development experience. I haven't done anything particularly unusual.

How do you test Google Apps Script?

Isn't it hard to debug by accessing script.google.com?

Google Apps Script Debugging ...
Google Apps Script Debugging ...
  • It's slow because you're stepping over the network
  • It's troublesome to adjust the service side (like preparing data) when integrating with G Suite services
  • The debug function is poor

It's very stressful. It's fine for a simple GAS, but if you want to create a slightly complex GAS, you'll feel it's a problem.

Let's run it locally

Google has released a command-line tool called Clasp that allows you to run GAS in a local environment.

https://github.com/google/clasp

Also, since Clasp supports Typescript, it is now possible to code focusing on types.

https://www.npmjs.com/package/@types/google-apps-script

Choosing Typescript makes interface design easier. Of course, I think the same can be achieved with .gs files.

Next, by combining with a testing tool called Jest, testing is possible in a local environment.

https://jestjs.io/docs/getting-started

However, you can't just write test code. For example, when coding a test to get a calendar event, suppose you write the following script.

const calendar: Calendar = CalendarApp.getCalendarById(
  "<your google calendar id>"
);
calendar
  .getEvents(new Date("2020-01-01"), new Date("2020-01-02"))
  .forEach((calendarEvent: CalendarEvent) => {
    console.log(calendarEvent.getTitle());
  });

If you write it like this, it will actually go to get the real calendar event. In a test, you would want to avoid such processing. Therefore, to replace CalendarApp with a fake object, or a Mock object, we apply the principle of dependency inversion.

interface ICalendarApp {
  calendars?: Array<ICalendar>;
  getCalendarById(id: string): ICalendar;
}

interface ICalendar {
  calendarEvents?: Array<ICalendarEvent>;
  getEvents(startTime: Date, endTime: Date): Array<ICalendarEvent>;
}

interface ICalendarEvent {
  title?: string;
  getTitle(): string;
}

class CalendarAppMock implements ICalendarApp {
  calendars?: Array<ICalendar>;

  getCalendarById(id: string): ICalendar {
    return this.calendars![0].calendar;
  }
}

class CalendarAppImpl implements ICalendarApp {
  getCalendarById(id: string): ICalendar {
    const calendar: ICalendar = CalendarApp.getCalendarById(id);
    return calendar;
  }
}

Prepare such an interface/class and modify the previous code as follows.

const calendar: ICalendar = new CalendarAppMock().getCalendarById();
calendar
  .getEvents(new Date("2020-01-01"), new Date("2020-01-02"))
  .forEach((calendarEvent: ICalendarEvent) => {
    console.log(calendarEvent.getTitle());
  });

As a result, you can now insert a Mock object instead of CalendarApp. This makes local testing possible.

Of course, in product code, you should use CalendarAppImpl instead of CalendarAppMock. If the number of objects to be replaced with Mock increases, you might want to consider a DI container like InversifyJS.

https://github.com/inversify/InversifyJS

By doing this, tests by Jest will work.
In fact, I was able to fully test the library I developed and published.

https://www.npmjs.com/package/@silverbirder/caat

CaAT $ npm run test -- --coverage

> jest "--coverage"

 PASS  __tests__/utils/dateUtils.test.ts
 PASS  __tests__/group/groupImpl.test.ts
 PASS  __tests__/member/memberImpl.test.ts
---------------------|---------|----------|---------|---------|-------------------
File                 | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files            |   98.43 |    97.62 |   96.67 |   98.37 |
 __tests__           |     100 |      100 |     100 |     100 |
  generator.ts       |     100 |      100 |     100 |     100 |
 src/calendar        |    93.1 |      100 |   92.31 |   92.59 |
  calendarAppImpl.ts |      60 |      100 |      50 |      60 | 6,7
  calendarAppMock.ts |     100 |      100 |     100 |     100 |
 src/group           |     100 |      100 |     100 |     100 |
  groupImpl.ts       |     100 |      100 |     100 |     100 |
 src/member          |     100 |    94.74 |     100 |     100 |
  memberImpl.ts      |     100 |    94.74 |     100 |     100 | 38
 src/utils           |     100 |      100 |     100 |     100 |
  dateUtils.ts       |     100 |      100 |     100 |     100 |
---------------------|---------|----------|---------|---------|-------------------

Test Suites: 3 passed, 3 total
Tests:       23 passed, 23 total
Snapshots:   0 total
Time:        2.826s, estimated 6s
Ran all test suites.

The test of the function provided as a library is finished in just about 3 seconds. I was able to develop locally stress-free.

For more details, please see the source code of the library I actually created (__tests__).

Conclusion

GAS is very convenient. It improves productivity. You can quickly build APIs, and integration with G Suite is (of course) easy.

However, if the code becomes low in maintainability, it will become obsolete and no one will be able to take care of it. Test code is essential to always stay clean. Those who operate GAS, please consider test code.

Eh, wait a minute. Introduction of the library!

This is a library that I would like teams who are doing agile development and managing schedules with Google Calendar to use.

https://github.com/silverbirder/caat

CaAT is the Google Apps Script Library that Calculate the Assigned Time in Google Calendar.

What you can do with this tool is as follows.

  • Get the time (minutes) scheduled in a specific user's Google Calendar for a specified period
  • Overlapping appointments are considered as continuous appointments
  • Specified time/words are considered as non-calculable (such as lunch)
  • Who is taking a day off and when, get from all-day events

There is actually sample code, so please refer to it.

https://github.com/silverbirder/SampleCaat

If it was helpful, support me with a ☕!

Share

Related tags