When your Apps Script project is just one tiny function attached to a Sheet, you can get away with “click Run and hope for the best”.
But as soon as your codebase grows, multiple files, complex business rules, integrations with Sheets, Docs, external APIs, that approach stops scaling. A small change in one function can silently break something completely different.
That’s where unit testing in Google Apps Script comes in.
In this article, we’ll cover:
- What unit testing is and why it matters for Apps Script
- The current landscape of unit testing solutions for GAS (QUnitGS2, GasT, Utgs, Jest + clasp, etc.)
- Classic unit testing principles adapted to Apps Script (AAA, dependency injection)
- How to use QUnitGS2 / QUnit2GS to build a proper test suite in Apps Script
- A lightweight console-based test runner that runs all
test_functions and logs PASS/FAIL in the execution log
By the end, you’ll have both a full-featured QUnit setup and a “zero-UI” runner you can use as a quick safety net in any project.
What is unit testing and why is it important?
Unit testing is the practice of testing small, isolated pieces of code (units), usually at the function level.
A typical unit test:
- Calls a function with some inputs
- Compares the result to an expected value
- Fails loudly if they don’t match
In JavaScript (and therefore Apps Script), a unit test with QUnit looks like this:
QUnit.test('add() adds two numbers', (assert) => {
const result = add(1, 2);
assert.equal(result, 3, '1 + 2 should equal 3');
});Why unit testing is especially useful in Google Apps Script projects:
- Catch regressions early. When you refactor a Sheets automation or a Docs generator, tests tell you if something broke immediately instead of your users or clients discovering it later.
- Refactor with confidence. You can rename functions, split files, change logic – and rely on tests as a safety net.
- Executable documentation. A test like
test_shouldApplyDiscountForTotalsOver1000is more explicit than a vague comment buried in the code. - Faster debugging loops. Instead of manually re-running forms or simulating UI flows, you run the test suite and see exactly which scenario fails.
In “normal” JavaScript environments (Node/React), developers use tools like Jest, Mocha, QUnit, Ava… but Apps Script runs in a special environment that introduces a few constraints.
Limitations of Apps Script and existing unit testing solutions
Building a reliable unit testing structure in Google Apps Script (GAS) is essential for maintaining complex projects. But Apps Script is not a standard Node.js runtime:
- No native local file system
- No
npm testdirectly inside the IDE - Limited
setTimeout/ async patterns - Heavy reliance on global services like
SpreadsheetApp,DriveApp,MailApp, etc.
Because of that, unit testing solutions tend to fall into two big families:
- In-GAS frameworks – tests run on Google’s servers inside the Apps Script runtime
- Local testing – you pull code down with
claspand test it with Node tools, mocking Google services
In-GAS frameworks
These live directly in your Apps Script project.
| Approach | Framework / Tool | How it works | Pros | Cons |
| QUnit web UI | QUnitGS2 / QUnit2GS | Adaptation of the popular QUnit JS library. Tests run in GAS, results shown via a doGet() web app. | Familiar QUnit API, nice HTML report, well documented, works well with Apps Script web apps. | Requires deploying as a web app to see results; one more moving piece in your workflow. |
| TAP-style console | GasT | TAP-compatible testing framework; tests log TAP output to the execution log. | Simple setup, console-based output, easy to integrate into basic scripts. | Smaller community, less documentation than QUnit. |
| Minimal console libs | Utgs / bmUnitTest | Lightweight libs focused on simple assertions and PASS/FAIL logs in Logger.log. | Very easy to drop into any script, no web deployment required. | No GUI, you manage structure and conventions yourself. |
Local testing with clasp + Jest/Ava
With the clasp CLI you can clone your Apps Script project locally, and then use any Node test framework you like (Jest, Ava, etc.) with mocks for Google services.
- How it works
- Use
claspto pull your code locally. - Write Jest/Ava tests.
- Mock things like
SpreadsheetAppandUrlFetchApp. - Run
npm testlocally.
- Use
- Pros
- Full Node ecosystem (TypeScript, Jest watch mode, coverage reports, CI/CD, VS Code integration).
- Cons
- Higher setup cost, especially for mocking Google services accurately.
- Best suited to modular projects where business logic is already decoupled from Apps Script services.
Quick conclusion
If you want:
- A visual, browser-based test runner → QUnitGS2 / QUnit2GS is an excellent choice
- A blazing-fast, Node-like experience for pure logic → clasp + Jest is great.
- A quick in-editor check that runs functions and logs results → a small custom console runner (or Utgs/bmUnitTest) is the most direct option.
In the rest of this article, we’ll combine both worlds:
- QUnitGS2 for a “real” test suite with a UI
- A tiny log-based test runner you can drop into any project
Classic unit testing principles in the Apps Script world
Before we jump into QUnit and custom runners, it’s worth aligning on some basic testing principles and how they apply to Apps Script.
AAA: Arrange, Act, Assert
A readable test follows the AAA pattern:
- Arrange – set up inputs, initial state, mocks
- Act – call the function under test
- Assert – verify the result (return value or side effect)
Example (pseudo-code):
function test_addTwoNumbers() {
// Arrange
const a = 2;
const b = 3;
const expected = 5;
// Act
const actual = add(a, b);
// Assert
assert(actual, expected, 'add() should return 5 for (2, 3)');
}Labeling those three steps makes your Apps Script tests much easier to read and maintain.
Isolation and dependency injection in Apps Script
True unit tests should focus on one unit of logic at a time. That’s tricky in Apps Script because your code often calls global services directly:
function myFunction() {
const sheet = SpreadsheetApp.getActiveSpreadsheet(); // tightly coupled
// ...
}To make this testable, you can use dependency injection:
const fakeSpreadsheetService = {
getActiveSpreadsheet() {
return { /* fake sheet object */ };
},
};
// In your test:
myFunction(fakeSpreadsheetService);This pattern works great with both QUnitGS2 and a simple console runner.
Using QUnitGS2 (QUnit2GS) in Google Apps Script
Let’s start with a more “formal” test suite using QUnitGS2, which adapts the popular QUnit framework to Apps Script.
4.1. Install QUnitGS2 in your project
- Open your Apps Script project.
- Click add a library

3. Add the following script ID, and click Add:
1tXPhZmIyYiA_EMpTRJw0QpVGT5Pdb02PpOHCi9A9FFidblOc9CY_VLgG

A function to test
We’ll define a simple pure function to keep the focus on testing:
/**
* Calculates the total amount after applying a percentage discount.
*
* @param {number} amount - Base amount before discount.
* @param {number} discountPercent - Discount percentage between 0 and 100.
* @return {number} The discounted amount, rounded to 2 decimals.
* @throws {Error} If amount or discountPercent are invalid.
*/
function calculateDiscountedTotal(amount, discountPercent) {
if (typeof amount !== 'number' || Number.isNaN(amount) || amount < 0) {
throw new Error('amount must be a non-negative number');
}
if (
typeof discountPercent !== 'number' ||
Number.isNaN(discountPercent) ||
discountPercent < 0 ||
discountPercent > 100
) {
throw new Error('discountPercent must be between 0 and 100');
}
const discount = (amount * discountPercent) / 100;
const total = amount - discount;
return Number(total.toFixed(2));
}
Wire up the QUnit web app
Add this in (for example) QUnitRunner.gs:
/* global QUnitGS2 */
/**
* QUnit instance provided by QUnitGS2.
* @type {QUnit}
*/
const QUnit = QUnitGS2.QUnit;
/**
* Web app entry point that runs the QUnit test suite and returns HTML results.
*
* @param {Object} e - Web app request event.
* @return {Object} HTML output with QUnit UI.
*/
function doGet(e) {
QUnitGS2.init();
registerTests();
QUnit.start();
return QUnitGS2.getHtml();
}
/**
* Called from the client-side HTML to retrieve QUnit results
* after the server-side test run completes.
*
* @return {Object} JSON results from the QUnitGS2 server.
*/
function getResultsFromServer() {
return QUnitGS2.getResultsFromServer();
}Define QUnit tests
Now create registerTests() and write your tests:
/**
* Registers all QUnit test modules.
*/
function registerTests() {
QUnit.module('calculateDiscountedTotal', () => {
QUnit.test('returns original amount when discount is 0%', (assert) => {
const result = calculateDiscountedTotal(100, 0);
assert.equal(result, 100, '100 with 0% discount should stay 100');
});
QUnit.test('applies percentage discount correctly', (assert) => {
const result = calculateDiscountedTotal(200, 10);
assert.equal(result, 180, '10% discount on 200 should be 180');
});
QUnit.test('rounds to 2 decimals', (assert) => {
const result = calculateDiscountedTotal(99.99, 5);
assert.equal(result, 94.99, 'Should round result to 2 decimals');
});
QUnit.test('throws on negative amount', (assert) => {
assert.throws(
() => calculateDiscountedTotal(-10, 10),
/amount must be a non-negative number/,
'Negative amounts should throw'
);
});
QUnit.test('throws when discount > 100%', (assert) => {
assert.throws(
() => calculateDiscountedTotal(100, 150),
/discountPercent must be between 0 and 100/,
'Discount > 100% should throw'
);
});
});
}Deploy as a web app and run tests
- Deploy → New deployment → Web app
- Execute as: your account
- Who has access: as you prefer (e.g. “Only me”)
- Copy the web app URL and open it in your browser

QUnitGS2 will:
- Initialize QUnit
- Run your
registerTests() - Show a full QUnit HTML report with passing/failing tests

A simple console-based unit test runner (no web app required)
Sometimes you just want a quick pass/fail check in the Execution log without deploying a web app or integrating a full framework.
The script below implements a self-contained test runner that:
- Looks for all global functions whose names start with
test_ - Runs them one by one
- Logs
PASS/FAILand a short summary in the Apps Script execution log
// --- TEST UTILITIES: Place this in a file named 'TestRunner.gs' ---
/** @type {string} */
const TEST_PREFIX = 'test_';
/**
* Checks if two values are strictly equal and logs a PASS/FAIL message.
*
* @param {unknown} actual - The value returned by the function under test (Act).
* @param {unknown} expected - The predetermined correct value (Arrange).
* @param {string} message - Description of the assertion.
* @return {boolean} True if the assertion passes, false otherwise.
*/
function assert(actual, expected, message) {
if (actual === expected) {
Logger.log(`✅ PASS: ${message}`);
return true;
}
Logger.log(`❌ FAIL: ${message} - Expected: ${expected}, Actual: ${actual}`);
return false;
}
/**
* Iterates through all global functions and runs those prefixed with 'test_'.
* This acts as the central test runner.
*
* Run this manually from the Apps Script editor to execute the whole suite.
*/
function runAllTests() {
Logger.log('====================================');
Logger.log('🚀 STARTING UNIT TEST SUITE');
Logger.log('====================================');
let totalTests = 0;
let passedTests = 0;
let failedTests = 0;
const globalScope = typeof globalThis !== 'undefined' ? globalThis : this;
// Scan global functions and execute those starting with TEST_PREFIX.
for (const key in globalScope) {
if (Object.prototype.hasOwnProperty.call(globalScope, key)) {
const candidate = globalScope[key];
if (typeof candidate === 'function' && key.startsWith(TEST_PREFIX)) {
const testName = key;
Logger.log(`\n--- Running: ${testName} ---`);
try {
candidate();
passedTests++;
} catch (e) {
failedTests++;
const message = e instanceof Error ? e.message : String(e);
Logger.log(`💥 ERROR in ${testName}: ${message}`);
}
totalTests++;
}
}
}
Logger.log('\n====================================');
Logger.log(`RESULTS: ${totalTests} tests run`);
Logger.log(`PASSED: ${passedTests}`);
Logger.log(`FAILED: ${failedTests}`);
Logger.log('====================================');
}
- Paste
TestRunner.gsinto your Apps Script project. - Add tests by creating functions whose names start with
test_(in the same project). - In the editor, select
runAllTestsand click Run ▶️. - Open View → Logs to see a step-by-step PASS/FAIL log and the final summary.
This gives you:
- A very fast feedback loop while coding
- No web app deployment required
- A simple way to run a quick suite every time you touch the script
If you like this pattern, you can evolve it:
- Add more assertion helpers (
assertNotEquals,assertTrue,assertDeepEqual, …) - Add timing information per test
- Exit early if a critical test fails (for long suites)

Putting it together
There’s no single “right” way to do unit testing in Google Apps Script, but a good strategy looks like this:
- Use pure functions + dependency injection for your core business logic
- Cover that logic with tests – either with QUnitGS2 (web UI) or your own console runner
- Keep Apps Script service calls (
SpreadsheetApp,DriveApp, etc.) thin and easy to mock - For bigger projects or front-end code, consider clasp + Jest and a local toolchain
Start small: pick one critical module (invoice calculation, validation rules, date logic…), write a handful of tests, and get used to running them every time you make a change.
Once you’ve seen a refactor go through while your QUnitGS2 panel and/or console runner stays fully green, you’ll never want to go back to “click Run and cross your fingers” again.









