617 lines
14 KiB
Markdown
617 lines
14 KiB
Markdown
# @git.zone/tstest/tapbundle
|
|
|
|
> 🧪 Core TAP testing framework with enhanced assertions and lifecycle hooks
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
tap.test('should validate user input', async () => {
|
|
// test code
|
|
});
|
|
```
|
|
|
|
#### `tap.testParallel(description, testFunction)`
|
|
|
|
Define a test that runs in parallel with other parallel tests.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
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.
|
|
|
|
```typescript
|
|
tap.describe('User Authentication', () => {
|
|
tap.test('should login', async () => { });
|
|
tap.test('should logout', async () => { });
|
|
});
|
|
```
|
|
|
|
### Test Modes
|
|
|
|
#### Skip Tests
|
|
|
|
```typescript
|
|
tap.skip.test('not ready yet', async () => {
|
|
// This test will be skipped
|
|
});
|
|
```
|
|
|
|
#### Only Mode
|
|
|
|
```typescript
|
|
tap.only.test('focus on this test', async () => {
|
|
// Only tests marked with 'only' will run
|
|
});
|
|
```
|
|
|
|
#### Todo Tests
|
|
|
|
```typescript
|
|
tap.todo.test('implement feature X');
|
|
```
|
|
|
|
### Fluent Test Builder
|
|
|
|
Chain test configurations for expressive test definitions:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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](https://code.foss.global/push.rocks/smartexpect) that automatically generates diffs for failed assertions.
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```bash
|
|
TSTEST_FILTER_TAGS=unit tstest test/mytest.node.ts
|
|
```
|
|
|
|
### TapTools
|
|
|
|
Each test receives a `tapTools` instance with utilities:
|
|
|
|
#### Test Control Methods
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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:
|
|
```bash
|
|
UPDATE_SNAPSHOTS=true tstest test/mytest.ts
|
|
```
|
|
|
|
## Advanced Features
|
|
|
|
### Pre-Tasks and Post-Tasks
|
|
|
|
Run setup and teardown tasks before/after all tests:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Always export `tap.start()`** at the end of test files:
|
|
```typescript
|
|
export default tap.start();
|
|
```
|
|
|
|
2. **Use descriptive test names** that explain what is being tested:
|
|
```typescript
|
|
tap.test('should return 404 when user does not exist', async () => { });
|
|
```
|
|
|
|
3. **Group related tests** with `describe()` blocks:
|
|
```typescript
|
|
tap.describe('User validation', () => {
|
|
// All user validation tests
|
|
});
|
|
```
|
|
|
|
4. **Leverage lifecycle hooks** to reduce duplication:
|
|
```typescript
|
|
tap.beforeEach(async () => {
|
|
// Common setup
|
|
});
|
|
```
|
|
|
|
5. **Tag tests appropriately** for flexible test execution:
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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.
|