15 KiB
@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
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who want to sign a contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
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
});
License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the license file within this repository.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
Company Information
Task Venture Capital GmbH Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.