Files
tstest/ts_tapbundle
..

@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

  1. Always export tap.start() at the end of test files:

    export default tap.start();
    
  2. Use descriptive test names that explain what is being tested:

    tap.test('should return 404 when user does not exist', async () => { });
    
  3. Group related tests with describe() blocks:

    tap.describe('User validation', () => {
      // All user validation tests
    });
    
  4. Leverage lifecycle hooks to reduce duplication:

    tap.beforeEach(async () => {
      // Common setup
    });
    
  5. 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
});

This project is licensed under MIT.

© 2025 Task Venture Capital GmbH. All rights reserved.