Testing Framework Developer Documentation¶
- Testing Framework Developer Documentation
- Overview
- Key Features
- Framework Architecture
- Test Suite Structure and Best Practices
- Getting Started
- Writing Tests
- GAS Mocks
- Helper Functions
- Running Tests
- API Reference
Overview¶
The JsonDbApp testing framework uses Vitest with realistic Google Apps Script (GAS) API mocks to provide fast, reliable unit testing in a local Node.js environment. Tests run against realistic implementations of DriveApp, PropertiesService, LockService and other GAS APIs that write to disk, ensuring high-fidelity test behaviour without requiring deployment to the Apps Script platform.
Key Features¶
- Vitest-based: Modern, fast test runner with excellent DX
- Realistic GAS Mocks: Local implementations of DriveApp, PropertiesService, LockService, Utilities that persist to disk
- TDD-Ready: Red-Green-Refactor workflow with watch mode support
- Isolated Test Environment: Each test uses isolated ScriptProperties keys and Drive folders
- Comprehensive Assertions: Vitest's built-in matchers plus custom helpers
- Lifecycle Hooks:
beforeEach,afterEach,beforeAll,afterAllfor setup and teardown - Resource Cleanup: Automatic tracking and cleanup of test artefacts
Framework Architecture¶
The testing framework consists of several layers:
- Vitest: Test runner and assertion library
- GAS Mocks (tools/gas-mocks/): Node.js implementations of Google Apps Script APIs
- Setup Files (tests/setup/): Bootstrap GAS mocks and load legacy source files
- Test Helpers (tests/helpers/): Reusable setup, teardown, and utility functions
- Test Suites (tests/unit/): Organised test files by component
Test Suite Structure and Best Practices¶
All tests follow a consistent, modular structure:
- One feature per file: Each test file focuses on a specific component or feature (e.g., MasterIndex.test.js, database-collection-management.test.js)
- Descriptive test names: Use
describe()blocks to group related tests andit()for individual test cases - Arrange-Act-Assert: Each test should clearly separate setup, execution, and assertions
- Lifecycle hooks: Use
beforeEachandafterEachfor resource management and isolation - Descriptive assertions: Use Vitest's
expect()with clear matcher names - No side effects: Always clean up files, folders, and ScriptProperties, even on failure
- Red-Green-Refactor: Write failing tests first, then minimal passing code, then refactor
- Coverage: Include tests for constructor validation, configuration, happy paths, error cases, edge cases, and resource cleanup
Example test structure:
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { setupComponent, cleanupComponent } from '../helpers/component-test-helpers.js';
describe('Component Feature', () => {
let component;
beforeEach(() => {
component = setupComponent();
});
afterEach(() => {
cleanupComponent(component);
});
it('should perform expected behaviour', () => {
// Arrange
const input = { value: 42 };
// Act
const result = component.process(input);
// Assert
expect(result.value).toBe(42);
expect(result.processed).toBe(true);
});
});
Getting Started¶
Directory Structure¶
tests/
├── vitest.config.js # Vitest configuration
├── setup/
│ └── gas-mocks.setup.js # Bootstraps GAS mocks and loads source files
├── helpers/
│ ├── database-test-helpers.js
│ ├── collection-test-helpers.js
│ └── ... # Reusable test utilities
├── unit/
│ ├── master-index/
│ ├── database/
│ ├── validation/
│ └── ... # Component-specific test suites
└── .gas-drive/ # Mock Drive storage (gitignored)
└── .gas-script-properties.json # Mock ScriptProperties (gitignored)
Configuration¶
The Vitest configuration (tests/vitest.config.js) sets up:
- Test environment (Node.js)
- Setup files (GAS mocks)
- Test file patterns (
unit/**/*.test.js,helpers/**/*.test.js) - Mock cleanup behaviour
GAS Mock Setup¶
The setup file (tests/setup/gas-mocks.setup.js):
- Creates GAS mock instances with isolated storage paths
- Injects mocks into global scope (
DriveApp,PropertiesService, etc.) - Loads legacy source files into the test context using
vm.runInThisContext()
Writing Tests¶
Basic Test Example¶
import { describe, it, expect } from 'vitest';
describe('IdGenerator', () => {
it('should generate unique IDs', () => {
const generator = new IdGenerator();
const id1 = generator.generateId();
const id2 = generator.generateId();
expect(id1).toBeDefined();
expect(id2).toBeDefined();
expect(id1).not.toBe(id2);
});
});
Test with Setup and Cleanup¶
import { describe, it, expect, afterEach } from 'vitest';
const scriptProperties = PropertiesService.getScriptProperties();
const trackedKeys = new Set();
const registerKey = (key) => {
trackedKeys.add(key);
return key;
};
afterEach(() => {
for (const key of trackedKeys) {
scriptProperties.deleteProperty(key);
}
trackedKeys.clear();
});
describe('MasterIndex Persistence', () => {
it('should persist to ScriptProperties', () => {
const key = registerKey(`TEST_KEY_${Date.now()}`);
const masterIndex = new MasterIndex({ masterIndexKey: key });
const stored = scriptProperties.getProperty(key);
expect(stored).toBeDefined();
expect(typeof stored).toBe('string');
});
});
Test with Helper Functions¶
Database Test Helpers Example:
import { describe, it, expect } from 'vitest';
import {
setupInitialisedDatabase,
generateUniqueName,
registerDatabaseFile
} from '../../helpers/database-test-helpers.js';
describe('Database Collection Management', () => {
it('should create a new collection', () => {
const { database } = setupInitialisedDatabase();
const name = generateUniqueName('testCollection');
const collection = database.createCollection(name);
registerDatabaseFile(collection.driveFileId);
expect(collection.name).toBe(name);
expect(database.listCollections()).toContain(name);
});
});
Collection Test Helpers Example:
import { describe, it, expect } from 'vitest';
import {
createIsolatedTestCollection,
seedStandardEmployees,
assertAcknowledgedWrite
} from '../../helpers/collection-test-helpers.js';
describe('Collection Delete Operations', () => {
it('should delete a document by ID', () => {
// Arrange
const { collection } = createIsolatedTestCollection('deleteTest');
const { aliceId } = seedStandardEmployees(collection);
// Act
const result = collection.deleteOne({ _id: aliceId });
// Assert
assertAcknowledgedWrite(result, { deletedCount: 1 });
expect(collection.findOne({ _id: aliceId })).toBeNull();
});
});
Error Testing¶
describe('Error Handling', () => {
it('should throw InvalidArgumentError for invalid input', () => {
const { database } = setupInitialisedDatabase({ autoCreateCollections: false });
const missingName = generateUniqueName('missing');
expect(() => database.getCollection(missingName)).toThrowError(/auto-create is disabled/);
});
});
GAS Mocks¶
The GAS mocks (tools/gas-mocks/gas-mocks.cjs) provide realistic implementations of:
DriveApp¶
createFolder(name): Creates folder on diskgetFolderById(id): Retrieves folder by IDgetFileById(id): Retrieves file by IDgetRootFolder(): Returns singleton root folder
Folder¶
createFile(name, content, mimeType): Writes file to diskgetFiles(): Returns FileIteratorgetFoldersByName(name): Returns FolderIteratorsetTrashed(trashed): Marks folder as deleted
File¶
getName(),getId(),getMimeType(): Metadata accessorsgetBlob(): Returns Blob withgetDataAsString()setContent(content): Updates file content on disksetTrashed(trashed): Marks file as deleted
PropertiesService¶
getScriptProperties(): Returns singleton Properties instance- Properties:
getProperty(key),setProperty(key, value),deleteProperty(key) - Backed by JSON file on disk
LockService¶
getScriptLock(): Returns singleton Lock instance- Lock:
waitLock(timeout),releaseLock() - Note: Uses busy-wait, suitable for single-threaded sequential tests only
Utilities¶
sleep(milliseconds): Blocking sleep
Logger¶
log(data): Forwards to console
MimeType¶
PLAIN_TEXT:"text/plain"JSON:"application/json"
Configuration:
const mocks = createGasMocks({
driveRoot: '/tmp/gasdb-drive', // Where Drive files are stored
propertiesFile: '/tmp/gasdb-props.json' // Where ScriptProperties are persisted
});
Helper Functions¶
Test helpers provide reusable setup and cleanup utilities:
Database Helpers¶
(tests/helpers/database-test-helpers.js)
cleanupDatabaseTests(): Removes Drive files and ScriptProperties keys created during Database testscreateBackupIndexFile(rootFolderId, backupData, fileName): Creates a Drive backup file for recovery scenarioscreateDatabaseTestConfig(overrides): Builds isolated configuration objects for Database testsexpectCollectionPersisted(databaseContext, collectionName, expectedMetadata): Verifies that a collection has been persisted to the MasterIndex with expected metadata (fileId, documentCount). Automatically registers the file for cleanup and instantiates MasterIndex for assertionsgenerateUniqueName(prefix): Generates unique names for artefactsregisterDatabaseFile(fileId): Tracks files for cleanupregisterMasterIndexKey(masterIndexKey): Registers ScriptProperties keys for cleanupsetupDatabaseTestEnvironment(overrides): Constructs Database instances with isolated storagesetupInitialisedDatabase(overrides): Creates Database instances that already executed createDatabase() and initialise()
Collection Helpers¶
(tests/helpers/collection-test-helpers.js)
assertAcknowledgedWrite(result, expectedCounts): Validates MongoDB-style write results with optional count assertions (matchedCount, modifiedCount, deletedCount, insertedId)createIsolatedTestCollection(collectionName): Builds fresh environment and returns env, collection, and file IDcreateMasterIndexKey(): Creates unique master index key with auto-cleanupcreateTestCollection(env, collectionName, options): Creates Collection instance with registrationcreateTestCollectionFile(folderId, collectionName): Creates collection filecreateTestFileWithContent(folderId, fileName, content): Creates file with custom contentcreateTestFolder(): Creates test folder in mock Drive with auto-cleanupregisterAndCreateCollection(env, collectionName, fileId, documentCount): Registers metadata and creates CollectionseedStandardEmployees(collection): Seeds collection with standard employee test data (Alice, Bob, Charlie) and returns object containing insertedId valuessetupCollectionTestEnvironment(): Complete environment setup (folder, master index, file service, database)
DocumentOperations Helpers¶
(tests/helpers/document-operations-test-helpers.js)
createDocumentOperationsContext(): Creates complete test context with env, docOps, and reload helper (replaces beforeEach setup)setupTestEnvironment(): Sets up complete test environment for DocumentOperations tests (returns env with folderId, fileId, collection, logger)resetCollection(collection): Resets a collection to initial empty statecreateTestFolder(): Creates a test folder in mock Drive with auto-cleanupcreateTestCollectionFile(folderId, collectionName): Creates a test collection file in the specified folderassertAcknowledgedResult(result, expectedCounts): Asserts that a DocumentOperations result is acknowledged and optionally checks modifiedCount/deletedCountcleanupTestResources(): Cleanup function automatically registered with afterEach
Usage Pattern:
The createDocumentOperationsContext() helper simplifies test setup by providing a complete context in one call:
import { describe, it, expect, beforeEach } from 'vitest';
import { createDocumentOperationsContext } from '../../helpers/document-operations-test-helpers.js';
describe('DocumentOperations Tests', () => {
let docOps, reload;
beforeEach(() => {
({ docOps, reload } = createDocumentOperationsContext());
});
it('should insert and persist document', () => {
// Arrange
const testDoc = { name: 'Test User', email: 'test@example.com' };
// Act
const result = docOps.insertDocument(testDoc);
// Assert
expect(result._id).toBeDefined();
expect(result.name).toBe(testDoc.name);
// Verify persistence
const documents = reload();
const savedDoc = documents[result._id];
expect(savedDoc).toBeDefined();
expect(savedDoc.name).toBe(testDoc.name);
});
});
The reload() helper function (returned by createDocumentOperationsContext()) reloads collection data from disk and returns the current documents object, making it easy to verify persistence.
Validation Helpers¶
(tests/helpers/validation-test-helpers.js)
The describeValidationOperatorSuite() helper simplifies validation test setup by providing automatic environment setup and cleanup:
import { describe, it, expect } from 'vitest';
import { describeValidationOperatorSuite } from '../../helpers/validation-test-helpers.js';
describeValidationOperatorSuite('$eq Equality Operator Tests', (getTestEnv) => {
describe('Basic equality matching', () => {
it('should match string values exactly', () => {
// Arrange
const testEnv = getTestEnv();
const collection = testEnv.collections.persons;
// Act
const results = collection.find({ 'name.first': { $eq: 'Anna' } });
// Assert
expect(results).toHaveLength(1);
expect(results[0]._id).toBe('person1');
});
});
});
The getTestEnv() helper function (provided by describeValidationOperatorSuite()) retrieves the test environment with pre-populated collections and ValidationMockData, making it easy to test query and update operators.
Database Helpers¶
(tests/helpers/database-test-helpers.js)
The expectCollectionPersisted() helper verifies that collections are properly persisted to the MasterIndex:
import { describe, it, expect } from 'vitest';
import {
setupInitialisedDatabase,
expectCollectionPersisted
} from '../../helpers/database-test-helpers.js';
describe('Database Collection Management', () => {
it('should persist collection to master index', () => {
// Arrange
const { database, ...databaseContext } = setupInitialisedDatabase();
const collectionName = 'users';
// Act
const collection = database.createCollection(collectionName);
// Assert
expectCollectionPersisted(databaseContext, collectionName, {
fileId: collection.driveFileId,
documentCount: 0
});
});
});
The expectCollectionPersisted() helper automatically registers the file for cleanup, instantiates a fresh MasterIndex, and verifies all metadata properties.
MasterIndex Helpers¶
(tests/helpers/master-index-test-helpers.js)
cleanupMasterIndexTests(): Deletes all registered ScriptProperties keys after each testcreateMasterIndexKey(): Generates and registers a unique ScriptProperties key for testscreateTestMasterIndex(config): Builds an isolated MasterIndex with automatic key tracking; accepts overrides such asmodificationHistoryLimitso history trimming can be exercised deterministicallyregisterMasterIndexKey(key): Adds an existing key to the tracked cleanup setseedMasterIndex(key, data): Serialises and stores master index payloads for fixtures; pair with CollectionMetadata instances when validating the metadata normaliser
Validation Helpers¶
(tests/helpers/validation-test-helpers.js)
cleanupValidationTests(env): Cleans up all validation test resources (files, folders, ScriptProperties)describeValidationOperatorSuite(description, callback): Creates a complete validation test suite with automatic setup/cleanup. ProvidesgetTestEnv()function to access the test environment (database, collections, mock data)setupValidationTestEnvironment(): Sets up a complete validation test environment with pre-populated collections and mock data
When writing MasterIndex suites, prefer the public API so the internal helpers are exercised end to end. This ensures metadata cloning, timestamp coercion, lock refresh behaviour, and persistence semantics mirror production behaviour.
For cross-instance coordination regressions, construct separate MasterIndex instances that share the same masterIndexKey. This is the preferred way to prove that ScriptLock-protected mutations reload the latest ScriptProperties snapshot instead of acting on stale in-memory state.
For example:
const { masterIndex } = createTestMasterIndex({ modificationHistoryLimit: 5 });
masterIndex.addCollection('users', { fileId: 'users-file' });
for (let i = 0; i < 10; i += 1) {
masterIndex.updateCollectionMetadata('users', { documentCount: i });
}
const history = masterIndex.getModificationHistory('users');
expect(history).toHaveLength(5);
DEFAULT_MODIFICATION_HISTORY_LIMIT is exported alongside the MasterIndex facade; use it when you need to assert the fallback cap without overriding configuration.
Running Tests¶
Run All Tests¶
Run All Tests (Verbose Output)¶
Watch Mode¶
Run Specific Test File¶
Run Tests Matching Pattern¶
Coverage¶
API Reference¶
Vitest Core APIs¶
Test Structure¶
describe(name, fn): Groups related testsit(name, fn)/test(name, fn): Defines individual testbeforeEach(fn): Runs before each test in scopeafterEach(fn): Runs after each test in scopebeforeAll(fn): Runs once before all tests in scopeafterAll(fn): Runs once after all tests in scope
Assertions¶
Vitest uses expect() with matchers:
Equality¶
expect(value).toBe(expected): Strict equality (===)expect(value).toEqual(expected): Deep equalityexpect(value).not.toBe(expected): Negation
Truthiness¶
expect(value).toBeTruthy(): Truthy valueexpect(value).toBeFalsy(): Falsy valueexpect(value).toBeDefined(): Not undefinedexpect(value).toBeUndefined(): Undefinedexpect(value).toBeNull(): Null
Numbers¶
expect(value).toBeGreaterThan(n)expect(value).toBeLessThan(n)expect(value).toBeCloseTo(n, precision)
Strings¶
expect(string).toMatch(pattern): Regex or substring matchexpect(string).toContain(substring)
Arrays/Iterables¶
expect(array).toContain(item)expect(array).toHaveLength(n)expect(array).toEqual(expect.arrayContaining([...]))
Objects¶
expect(obj).toHaveProperty(key, value)expect(obj).toMatchObject(subset)expect(obj).toBeInstanceOf(Class)
Exceptions¶
expect(() => fn()).toThrow(): Throws any errorexpect(() => fn()).toThrow(ErrorClass): Throws specific error typeexpect(() => fn()).toThrowError(message): Throws with message matching string/regex
Mocking (if needed)¶
vi.fn(): Creates mock functionvi.spyOn(object, 'method'): Spies on methodvi.mock(path): Mocks module
GAS Mock APIs¶
See GAS Mocks Plan for complete method signatures and data shapes.