@git.zone/tstest/tapbundle
🧪 Core TAP testing framework with enhanced assertions and lifecycle hooks
Installation
# tapbundle is typically included as part of @git.zone/tstest
pnpm install --save-dev @git.zone/tstest
Overview
@git.zone/tstest/tapbundle is the core testing framework module that provides the TAP (Test Anything Protocol) implementation for tstest. It offers a comprehensive API for writing and organizing tests with support for lifecycle hooks, test suites, enhanced assertions with diff generation, and flexible test configuration.
Key Features
- 🎯 TAP Protocol Compliant - Full TAP version 13 support
- 🔍 Enhanced Assertions - Built on smartexpect with automatic diff generation
- 🏗️ Test Suites - Organize tests with
describe()blocks - 🔄 Lifecycle Hooks - beforeEach/afterEach at suite and global levels
- 🏷️ Test Tagging - Filter tests by tags for selective execution
- ⚡ Parallel Testing - Run tests concurrently with
testParallel() - 🔁 Automatic Retries - Configure retry logic for flaky tests
- ⏱️ Timeout Control - Set timeouts at global, file, or test level
- 🎨 Fluent API - Chain test configurations with builder pattern
- 📊 Protocol Events - Real-time test execution events
Basic Usage
Simple Test File
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.test('should add numbers correctly', async () => {
const result = 2 + 2;
expect(result).toEqual(4);
});
export default tap.start();
Using Test Suites
import { tap, expect } from '@git.zone/tstest/tapbundle';
tap.describe('Calculator', () => {
tap.beforeEach(async (tapTools) => {
// Setup before each test in this suite
});
tap.test('should add', async () => {
expect(2 + 2).toEqual(4);
});
tap.test('should subtract', async () => {
expect(5 - 3).toEqual(2);
});
tap.afterEach(async (tapTools) => {
// Cleanup after each test in this suite
});
});
export default tap.start();
API Reference
Main Test Methods
tap.test(description, testFunction)
Define a standard test that runs sequentially.
tap.test('should validate user input', async () => {
// test code
});
tap.testParallel(description, testFunction)
Define a test that runs in parallel with other parallel tests.
tap.testParallel('should fetch user data', async () => {
// test code
});
Note: The tap.parallel().test() fluent API is now the recommended way to define parallel tests (see Fluent API section below).
tap.parallel()
Returns a fluent test builder configured for parallel execution.
tap.parallel().test('should fetch data', async () => {
// Parallel test
});
// With full configuration
tap.parallel()
.tags('api')
.retry(2)
.test('configured parallel test', async () => {});
tap.describe(description, suiteFunction)
Create a test suite to group related tests.
tap.describe('User Authentication', () => {
tap.test('should login', async () => { });
tap.test('should logout', async () => { });
});
Test Modes
Skip Tests
tap.skip.test('not ready yet', async () => {
// This test will be skipped
});
Only Mode
tap.only.test('focus on this test', async () => {
// Only tests marked with 'only' will run
});
Todo Tests
tap.todo.test('implement feature X');
Fluent Test Builder
Chain test configurations for expressive test definitions:
tap
.tags('integration', 'database')
.priority('high')
.retry(3)
.timeout(5000)
.test('should handle database connection', async () => {
// test with configured settings
});
Parallel Tests with Fluent API
Use tap.parallel() to create parallel tests with fluent configuration:
// Simple parallel test
tap.parallel().test('fetches user data', async () => {
// Runs in parallel with other parallel tests
});
// Parallel test with full configuration
tap
.parallel()
.tags('api', 'integration')
.retry(2)
.timeout(5000)
.test('should fetch data concurrently', async () => {
// Configured parallel test
});
Note: tap.parallel().test() is the recommended way to define parallel tests. The older tap.testParallel() method is still supported for backward compatibility.
Lifecycle Hooks
Suite-Level Hooks
tap.describe('Database Tests', () => {
tap.beforeAll(async (tapTools) => {
// Runs once before all tests in this suite
await initializeDatabaseConnection();
});
tap.beforeEach(async (tapTools) => {
// Runs before each test in this suite
await clearTestData();
});
tap.test('test 1', async () => { });
tap.test('test 2', async () => { });
tap.afterEach(async (tapTools) => {
// Runs after each test in this suite
});
tap.afterAll(async (tapTools) => {
// Runs once after all tests in this suite
await closeDatabaseConnection();
});
});
Global Hooks
tap.settings({
beforeAll: async () => {
// Runs once before all tests
},
afterAll: async () => {
// Runs once after all tests
},
beforeEach: async (testName) => {
// Runs before every test
},
afterEach: async (testName, passed) => {
// Runs after every test
}
});
Global Settings
Configure test behavior at the file level:
tap.settings({
timeout: 10000, // Default timeout for all tests
retries: 2, // Retry failed tests
retryDelay: 1000, // Delay between retries
bail: false, // Stop on first failure
suppressConsole: false, // Hide console output
verboseErrors: true, // Show full stack traces
showTestDuration: true, // Display test durations
maxConcurrency: 4, // Max parallel tests
});
Enhanced Assertions
The expect function is an enhanced wrapper around @push.rocks/smartexpect that automatically generates diffs for failed assertions.
import { expect } from '@git.zone/tstest/tapbundle';
tap.test('should compare objects', async () => {
const actual = { name: 'John', age: 30 };
const expected = { name: 'John', age: 31 };
// Will show a detailed diff of the differences
expect(actual).toEqual(expected);
});
Available Assertions
// Equality
expect(value).toEqual(expected);
expect(value).toBe(expected);
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
// Type checks
expect(value).toBeType('string');
// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');
// Arrays
expect(array).toContain(item);
// Exceptions
expect(fn).toThrow();
expect(fn).toThrow('error message');
// Async
await expect(promise).toResolve();
await expect(promise).toReject();
Test Tagging and Filtering
Tag tests for selective execution:
// Define tests with tags
tap.tags('integration', 'slow').test('complex test', async () => {
// test code
});
tap.tags('unit').test('fast test', async () => {
// test code
});
Filter tests by setting the environment variable:
TSTEST_FILTER_TAGS=unit tstest test/mytest.node.ts
TapTools
Each test receives a tapTools instance with utilities:
Test Control Methods
tap.test('test control examples', async (tapTools) => {
// Skip this test
tapTools.skip('reason');
// Conditionally skip
tapTools.skipIf(condition, 'reason');
// Mark test as skipped before execution
tapTools.markAsSkipped('reason');
// Mark as todo
tapTools.todo('not implemented');
// Allow test to fail without marking suite as failed
tapTools.allowFailure();
// Configure retries
tapTools.retry(3);
// Set timeout
tapTools.timeout(5000);
});
Utility Methods
tap.test('utility examples', async (tapTools) => {
// Delay execution
await tapTools.delayFor(1000); // Wait 1 second
await tapTools.delayForRandom(500, 1500); // Random delay
// Colored console output
tapTools.coloredString('✓ Success', 'green');
tapTools.coloredString('✗ Error', 'red');
});
Context and Data Sharing
tap.test('first test', async (tapTools) => {
// Store data in context
tapTools.context.set('userId', '12345');
// Store in testData property
tapTools.testData = { username: 'alice' };
});
tap.test('second test', async (tapTools) => {
// Retrieve from context
const userId = tapTools.context.get('userId');
// Check existence
if (tapTools.context.has('userId')) {
// Use data
}
// Clear context
tapTools.context.clear();
});
Fixtures
// Define a fixture globally (outside tests)
import { TapTools } from '@git.zone/tstest/tapbundle';
TapTools.defineFixture('database', async () => {
const db = await createTestDatabase();
return {
value: db,
cleanup: async () => await db.close()
};
});
// Use fixtures in tests
tap.test('database test', async (tapTools) => {
const db = await tapTools.fixture('database');
// Use db...
// Cleanup happens automatically
});
Factory Pattern
// Define a factory
TapTools.defineFixture('user', async () => {
return {
value: null, // Not used for factories
factory: async (data) => {
return await createUser(data);
},
cleanup: async (user) => await user.delete()
};
});
// Use factory in tests
tap.test('user test', async (tapTools) => {
const user = await tapTools.factory('user').create({ name: 'Alice' });
// Create multiple
const users = await tapTools.factory('user').createMany([
{ name: 'Alice' },
{ name: 'Bob' }
]);
// Cleanup happens automatically
});
Snapshot Testing
tap.test('snapshot test', async (tapTools) => {
const result = { name: 'Alice', age: 30 };
// Compare with stored snapshot
await tapTools.matchSnapshot(result);
// Named snapshots
await tapTools.matchSnapshot(result, 'user-data');
});
To update snapshots, run with:
UPDATE_SNAPSHOTS=true tstest test/mytest.ts
Advanced Features
Pre-Tasks and Post-Tasks
Run setup and teardown tasks before/after all tests:
tap.preTask('setup database', async () => {
// Runs before any tests
await initializeDatabase();
});
tap.test('first test', async () => {
// Database is ready
});
tap.test('second test', async () => {
// Tests run...
});
tap.postTask('cleanup database', async () => {
// Runs after all tests complete
await cleanupDatabase();
});
Note: Post tasks run after all tests but before the global afterAll hook.
Test Priority
Organize tests by priority level:
tap.priority('high').test('critical test', async () => { });
tap.priority('medium').test('normal test', async () => { });
tap.priority('low').test('optional test', async () => { });
Nested Suites
Create deeply nested test organization:
tap.describe('API', () => {
tap.describe('Users', () => {
tap.describe('GET /users', () => {
tap.test('should return all users', async () => { });
});
});
});
Protocol Events
Access real-time test events for custom tooling:
import { setProtocolEmitter } from '@git.zone/tstest/tapbundle';
// Get access to protocol emitter for custom event handling
// Events: test:started, test:completed, assertion:failed, suite:started, suite:completed
Additional Tap Methods
Configuration and Inspection
// Get current test settings
const settings = tap.getSettings();
console.log(settings.timeout, settings.retries);
// Explicitly fail a test
tap.test('validation test', async () => {
if (invalidCondition) {
tap.fail('Custom failure message');
}
});
Advanced Control
// Force stop test execution
tap.stopForcefully(exitCode, immediate);
// Handle thrown errors (internal use)
tap.threw(error);
Parallel Test Variants
In addition to tap.parallel().test(), skip/only/todo modes also support parallel execution:
// Skip parallel test
tap.skip.testParallel('not ready', async () => {});
// Only run this parallel test
tap.only.testParallel('focus here', async () => {});
// Todo parallel test
tap.todo.testParallel('implement later');
Note: Using tap.parallel() fluent API is recommended over these direct methods.
Best Practices
-
Always export
tap.start()at the end of test files:export default tap.start(); -
Use descriptive test names that explain what is being tested:
tap.test('should return 404 when user does not exist', async () => { }); -
Group related tests with
describe()blocks:tap.describe('User validation', () => { // All user validation tests }); -
Leverage lifecycle hooks to reduce duplication:
tap.beforeEach(async () => { // Common setup }); -
Tag tests appropriately for flexible test execution:
tap.tags('integration', 'database').test('...', async () => { });
TypeScript Support
tapbundle is written in TypeScript and provides full type definitions. The Tap class accepts a generic type for shared context:
interface MyTestContext {
db: DatabaseConnection;
user: User;
}
const tap = new Tap<MyTestContext>();
tap.test('should use context', async (tapTools) => {
// tapTools is typed with MyTestContext
});
Legal
This project is licensed under MIT.
© 2025 Task Venture Capital GmbH. All rights reserved.