Files
tstest/ts_tapbundle/readme.md

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

  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 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.