2022-02-14 23:50:07 +01:00
2018-08-04 15:51:44 +02:00
2022-03-13 00:30:32 +01:00
2022-03-13 00:30:32 +01:00
2023-08-26 15:42:18 +02:00
2022-02-14 15:40:11 +01:00
2025-05-24 00:59:30 +00:00
2023-11-09 17:55:26 +01:00

@gitzone/tstest

🧪 A powerful, modern test runner for TypeScript - making your test runs beautiful and informative!

Why tstest?

tstest is a TypeScript test runner that makes testing delightful. It's designed for modern development workflows with beautiful output, flexible test execution, and powerful features that make debugging a breeze.

Key Features

  • 🎯 Smart Test Execution - Run all tests, single files, or use glob patterns
  • 🎨 Beautiful Output - Color-coded results with emojis and clean formatting
  • 📊 Multiple Output Modes - Choose from normal, quiet, verbose, or JSON output
  • 🔍 Automatic Discovery - Finds all your test files automatically
  • 🌐 Cross-Environment - Supports Node.js and browser testing
  • 📝 Detailed Logging - Optional file logging for debugging
  • Performance Metrics - See which tests are slow
  • 🤖 CI/CD Ready - JSON output mode for automation
  • 🏷️ Tag-based Filtering - Run only tests with specific tags
  • 🎯 Parallel Test Execution - Run tests in parallel groups
  • 🔧 Test Lifecycle Hooks - beforeEach/afterEach support
  • 📸 Snapshot Testing - Compare test outputs with saved snapshots
  • Timeout Control - Set custom timeouts for tests
  • 🔁 Retry Logic - Automatically retry failing tests
  • 🛠️ Test Fixtures - Create reusable test data
  • 📦 Browser-Compatible - Full browser support with embedded tapbundle

Installation

npm install --save-dev @gitzone/tstest
# or with pnpm
pnpm add -D @gitzone/tstest

Usage

Basic Test Execution

# Run all tests in a directory
tstest test/

# Run a specific test file
tstest test/test.mycomponent.ts

# Use glob patterns
tstest "test/**/*.spec.ts"
tstest "test/unit/*.ts"

Execution Modes

tstest intelligently detects how you want to run your tests:

  1. Directory mode - Recursively finds all test files
  2. File mode - Runs a single test file
  3. Glob mode - Uses pattern matching for flexible test selection

Command Line Options

Option Description
--quiet, -q Minimal output - perfect for CI environments
--verbose, -v Show all console output from tests
--no-color Disable colored output
--json Output results as JSON
--logfile Save detailed logs with automatic error and diff tracking
--tags <tags> Run only tests with specific tags (comma-separated)
--timeout <seconds> Timeout test files after specified seconds
--startFrom <n> Start running from test file number n
--stopAt <n> Stop running at test file number n

Example Outputs

Normal Output (Default)

🔍 Test Discovery
   Mode: directory
   Pattern: test
   Found: 4 test file(s)

▶️  test/test.ts (1/4)
   Runtime: node.js
   ✅ prepare test (1ms)
   Summary: 1/1 PASSED

📊 Test Summary
┌────────────────────────────────┐
│ Total Files:                 4 │
│ Total Tests:                 4 │
│ Passed:                      4 │
│ Failed:                      0 │
│ Duration:                542ms │
└────────────────────────────────┘

ALL TESTS PASSED! 🎉

Quiet Mode

Found 4 tests
✅ test functionality works
✅ api calls return expected data
✅ error handling works correctly
✅ performance is within limits

Summary: 4/4 | 542ms | PASSED

Verbose Mode

Shows all console output from your tests, making debugging easier:

▶️  test/api.test.ts (1/1)
   Runtime: node.js
   Making API call to /users...
   Response received: 200 OK
   Processing user data...
   ✅ api calls return expected data (145ms)
   Summary: 1/1 PASSED

JSON Mode

Perfect for CI/CD pipelines:

{"event":"discovery","count":4,"pattern":"test","executionMode":"directory"}
{"event":"fileStart","filename":"test/test.ts","runtime":"node.js","index":1,"total":4}
{"event":"testResult","testName":"prepare test","passed":true,"duration":1}
{"event":"summary","summary":{"totalFiles":4,"totalTests":4,"totalPassed":4,"totalFailed":0,"totalDuration":542}}

Test File Naming Conventions

tstest supports different test environments through file naming:

Pattern Environment Example
*.ts Node.js (default) test.basic.ts
*.node.ts Node.js only test.api.node.ts
*.chrome.ts Chrome browser test.dom.chrome.ts
*.browser.ts Browser environment test.ui.browser.ts
*.both.ts Both Node.js and browser test.isomorphic.both.ts

Writing Tests with tapbundle

tstest includes tapbundle, a powerful TAP-based test framework. Import it from the embedded tapbundle:

import { expect, tap } from '@git.zone/tstest/tapbundle';

tap.test('my awesome test', async () => {
  const result = await myFunction();
  expect(result).toEqual('expected value');
});

tap.start();

Module Exports

tstest provides multiple exports for different use cases:

  • @git.zone/tstest - Main CLI and test runner functionality
  • @git.zone/tstest/tapbundle - Browser-compatible test framework
  • @git.zone/tstest/tapbundle_node - Node.js-specific test utilities

tapbundle Test Framework

Basic Test Syntax

import { tap, expect } from '@git.zone/tstest/tapbundle';

// Basic test
tap.test('should perform basic arithmetic', async () => {
  expect(2 + 2).toEqual(4);
});

// Async test with tools
tap.test('async operations', async (tools) => {
  await tools.delayFor(100); // delay for 100ms
  const result = await fetchData();
  expect(result).toBeDefined();
});

// Start test execution
tap.start();

Test Modifiers and Chaining

// Skip a test
tap.skip.test('not ready yet', async () => {
  // This test will be skipped
});

// Run only this test (exclusive)
tap.only.test('focus on this', async () => {
  // Only this test will run
});

// Todo test
tap.todo('implement later', async () => {
  // Marked as todo
});

// Chaining modifiers
tap.timeout(5000)
   .retry(3)
   .tags('api', 'integration')
   .test('complex test', async (tools) => {
     // Test with 5s timeout, 3 retries, and tags
   });

Test Organization with describe()

tap.describe('User Management', () => {
  let testDatabase;
  
  tap.beforeEach(async () => {
    testDatabase = await createTestDB();
  });
  
  tap.afterEach(async () => {
    await testDatabase.cleanup();
  });
  
  tap.test('should create user', async () => {
    const user = await testDatabase.createUser({ name: 'John' });
    expect(user.id).toBeDefined();
  });
  
  tap.describe('User Permissions', () => {
    tap.test('should set admin role', async () => {
      // Nested describe blocks
    });
  });
});

Test Tools (Available in Test Function)

Every test function receives a tools parameter with utilities:

tap.test('using test tools', async (tools) => {
  // Delay utilities
  await tools.delayFor(1000); // delay for 1000ms
  await tools.delayForRandom(100, 500); // random delay between 100-500ms
  
  // Skip test conditionally
  tools.skipIf(process.env.CI === 'true', 'Skipping in CI');
  
  // Skip test unconditionally
  if (!apiKeyAvailable) {
    tools.skip('API key not available');
  }
  
  // Mark as todo
  tools.todo('Needs implementation');
  
  // Retry configuration
  tools.retry(3); // Set retry count
  
  // Timeout configuration
  tools.timeout(10000); // Set timeout to 10s
  
  // Context sharing between tests
  tools.context.set('userId', 12345);
  const userId = tools.context.get('userId');
  
  // Deferred promises
  const deferred = tools.defer();
  setTimeout(() => deferred.resolve('done'), 100);
  await deferred.promise;
  
  // Colored console output
  const coloredString = await tools.coloredString('Success!', 'green');
  console.log(coloredString);
  
  // Error handling helper
  const error = await tools.returnError(async () => {
    throw new Error('Expected error');
  });
  expect(error).toBeInstanceOf(Error);
});

Snapshot Testing

tap.test('snapshot test', async (tools) => {
  const output = generateComplexOutput();
  
  // Compare with saved snapshot
  await tools.matchSnapshot(output);
  
  // Named snapshots for multiple checks in one test
  await tools.matchSnapshot(output.header, 'header');
  await tools.matchSnapshot(output.body, 'body');
});

// Update snapshots with: UPDATE_SNAPSHOTS=true tstest test/

Test Fixtures

// Define reusable fixtures
tap.defineFixture('testUser', async (data) => ({
  id: Date.now(),
  name: data?.name || 'Test User',
  email: data?.email || 'test@example.com',
  created: new Date()
}));

tap.defineFixture('testPost', async (data) => ({
  id: Date.now(),
  title: data?.title || 'Test Post',
  authorId: data?.authorId || 1
}));

// Use fixtures in tests
tap.test('fixture test', async (tools) => {
  const user = await tools.fixture('testUser', { name: 'John' });
  const post = await tools.fixture('testPost', { authorId: user.id });
  
  expect(post.authorId).toEqual(user.id);
  
  // Factory pattern for multiple instances
  const users = await tools.factory('testUser').createMany(5);
  expect(users).toHaveLength(5);
});

Parallel Test Execution

// Parallel tests within a file
tap.testParallel('parallel test 1', async () => {
  await heavyOperation();
});

tap.testParallel('parallel test 2', async () => {
  await anotherHeavyOperation();
});

// File naming for parallel groups
// test.api.para__1.ts - runs in parallel with other para__1 files
// test.db.para__1.ts - runs in parallel with other para__1 files
// test.auth.para__2.ts - runs after para__1 group completes

Assertions with expect()

tapbundle uses @push.rocks/smartexpect for assertions:

// Basic assertions
expect(value).toEqual(5);
expect(value).not.toEqual(10);
expect(obj).toDeepEqual({ a: 1, b: 2 });

// Type assertions
expect('hello').toBeTypeofString();
expect(42).toBeTypeofNumber();
expect(true).toBeTypeofBoolean();
expect([]).toBeArray();
expect({}).toBeTypeOf('object');

// Comparison assertions
expect(5).toBeGreaterThan(3);
expect(3).toBeLessThan(5);
expect(5).toBeGreaterThanOrEqual(5);
expect(5).toBeLessThanOrEqual(5);
expect(0.1 + 0.2).toBeCloseTo(0.3, 10);

// Truthiness
expect(true).toBeTrue();
expect(false).toBeFalse();
expect('text').toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(null).toBeNullOrUndefined();

// String assertions
expect('hello world').toStartWith('hello');
expect('hello world').toEndWith('world');
expect('hello world').toInclude('lo wo');
expect('hello world').toMatch(/^hello/);
expect('option').toBeOneOf(['choice', 'option', 'alternative']);

// Array assertions
expect([1, 2, 3]).toContain(2);
expect([1, 2, 3]).toContainAll([1, 3]);
expect([1, 2, 3]).toExclude(4);
expect([1, 2, 3]).toHaveLength(3);
expect([]).toBeEmptyArray();
expect([{ id: 1 }]).toContainEqual({ id: 1 });

// Object assertions
expect(obj).toHaveProperty('name');
expect(obj).toHaveProperty('user.email', 'test@example.com');
expect(obj).toHaveDeepProperty(['level1', 'level2']);
expect(obj).toMatchObject({ name: 'John' });

// Function assertions
expect(() => { throw new Error('test'); }).toThrow();
expect(() => { throw new Error('test'); }).toThrow(Error);
expect(() => { throw new Error('test error'); }).toThrowErrorMatching(/test/);
expect(myFunction).not.toThrow();

// Promise assertions
await expect(Promise.resolve('value')).resolves.toEqual('value');
await expect(Promise.reject(new Error('fail'))).rejects.toThrow();

// Custom assertions
expect(7).customAssertion(
  value => value % 2 === 1,
  'Value is not odd'
);

Pre-tasks

Run setup tasks before tests start:

tap.preTask('setup database', async () => {
  await initializeTestDatabase();
  console.log('Database initialized');
});

tap.preTask('load environment', async () => {
  await loadTestEnvironment();
});

// Pre-tasks run in order before any tests

Tag-based Test Filtering

// Tag individual tests
tap.tags('unit', 'api')
   .test('api unit test', async () => {
     // Test code
   });

tap.tags('integration', 'slow')
   .test('database integration', async () => {
     // Test code
   });

// Run only tests with specific tags
// tstest test/ --tags unit,api

Context Sharing

Share data between tests:

tap.test('first test', async (tools) => {
  const sessionId = await createSession();
  tools.context.set('sessionId', sessionId);
});

tap.test('second test', async (tools) => {
  const sessionId = tools.context.get('sessionId');
  expect(sessionId).toBeDefined();
  
  // Cleanup
  tools.context.delete('sessionId');
});

Browser Testing with webhelpers

For browser-specific tests:

import { tap, webhelpers } from '@git.zone/tstest/tapbundle';

tap.test('DOM manipulation', async () => {
  // Create DOM elements from HTML strings
  const element = await webhelpers.fixture(webhelpers.html`
    <div class="test-container">
      <h1>Test Title</h1>
      <button id="test-btn">Click Me</button>
    </div>
  `);
  
  expect(element.querySelector('h1').textContent).toEqual('Test Title');
  
  // Simulate interactions
  const button = element.querySelector('#test-btn');
  button.click();
});

tap.test('CSS testing', async () => {
  const styles = webhelpers.css`
    .test-class {
      color: red;
      font-size: 16px;
    }
  `;
  
  // styles is a string that can be injected into the page
  expect(styles).toInclude('color: red');
});

Advanced Error Handling

tap.test('error handling', async (tools) => {
  // Capture errors without failing the test
  const error = await tools.returnError(async () => {
    await functionThatThrows();
  });
  
  expect(error).toBeInstanceOf(Error);
  expect(error.message).toEqual('Expected error message');
});

Test Wrap

Create wrapped test environments:

import { TapWrap } from '@git.zone/tstest/tapbundle';

const tapWrap = new TapWrap({
  before: async () => {
    console.log('Before all tests');
    await globalSetup();
  },
  after: async () => {
    console.log('After all tests');
    await globalCleanup();
  }
});

// Tests registered here will have the wrap lifecycle
tapWrap.tap.test('wrapped test', async () => {
  // This test runs with the wrap setup/teardown
});

Advanced Features

Glob Pattern Support

Run specific test patterns:

# Run all unit tests
tstest "test/unit/**/*.ts"

# Run all integration tests
tstest "test/integration/*.test.ts"

# Run multiple patterns
tstest "test/**/*.spec.ts" "test/**/*.test.ts"

Important: Always quote glob patterns to prevent shell expansion. Without quotes, the shell will expand the pattern and only pass the first matching file to tstest.

Enhanced Test Logging

The --logfile option provides intelligent test logging with automatic organization:

tstest test/ --logfile

Log Organization:

  • Current Run: .nogit/testlogs/[testname].log
  • Previous Run: .nogit/testlogs/previous/[testname].log
  • Failed Tests: .nogit/testlogs/00err/[testname].log
  • Changed Output: .nogit/testlogs/00diff/[testname].log

Features:

  • Previous logs are automatically moved to the previous/ folder
  • Failed tests create copies in 00err/ for quick identification
  • Tests with changed output create diff reports in 00diff/
  • The 00err/ and 00diff/ folders are cleared on each run

Example Diff Report:

DIFF REPORT: test__api__integration.log
Generated: 2025-05-24T01:29:13.847Z
================================================================================

- [Line 8]    ✅ api test passes (150ms)
+ [Line 8]    ✅ api test passes (165ms)

================================================================================
Previous version had 40 lines
Current version has 40 lines

Test Timeout Protection

Prevent runaway tests with the --timeout option:

# Timeout any test file that runs longer than 60 seconds
tstest test/ --timeout 60

# Shorter timeout for unit tests
tstest test/unit/ --timeout 10

When a test exceeds the timeout:

  • The test process is terminated (SIGTERM)
  • The test is marked as failed
  • An error log is created in .nogit/testlogs/00err/
  • Clear error message shows the timeout duration

Test File Range Control

Run specific ranges of test files using --startFrom and --stopAt:

# Run tests starting from the 5th file
tstest test/ --startFrom 5

# Run only files 5 through 10
tstest test/ --startFrom 5 --stopAt 10

# Run only the first 3 test files
tstest test/ --stopAt 3

This is particularly useful for:

  • Debugging specific test failures in large test suites
  • Running tests in chunks on different CI runners
  • Quickly testing changes to specific test files

The output shows which files are skipped:

⏭️  test/auth.test.ts (1/10)
   Skipped: before start range (5)
⏭️  test/user.test.ts (2/10)
   Skipped: before start range (5)
▶️  test/api.test.ts (5/10)
   Runtime: node.js
   ✅ api endpoints work (145ms)

Performance Analysis

In verbose mode, see performance metrics:

⏱️  Performance Metrics:
   Average per test: 135ms
   Slowest test: api integration test (486ms)

Parallel Test Groups

Tests can be organized into parallel groups for concurrent execution:

━━━ Parallel Group: para__1 ━━━
▶️  test/auth.para__1.ts
▶️  test/user.para__1.ts
   ... tests run concurrently ...
──────────────────────────────────

━━━ Parallel Group: para__2 ━━━
▶️  test/db.para__2.ts
▶️  test/api.para__2.ts
   ... tests run concurrently ...
──────────────────────────────────

Files with the same parallel group suffix (e.g., para__1) run simultaneously, while different groups run sequentially.

CI/CD Integration

For continuous integration, combine quiet and JSON modes:

# GitHub Actions example
tstest test/ --json > test-results.json

# Or minimal output
tstest test/ --quiet

Advanced CI Example:

# Run tests with comprehensive logging and safety features
tstest test/ \
  --timeout 300 \
  --logfile \
  --json > test-results.json

# Run specific test chunks in parallel CI jobs
tstest test/ --startFrom 1 --stopAt 10   # Job 1
tstest test/ --startFrom 11 --stopAt 20  # Job 2
tstest test/ --startFrom 21              # Job 3

Debugging Failed Tests

When tests fail, use the enhanced logging features:

# Run with logging to capture detailed output
tstest test/ --logfile --verbose

# Check error logs
ls .nogit/testlogs/00err/

# Review diffs for flaky tests
cat .nogit/testlogs/00diff/test__api__endpoints.log

# Re-run specific failed tests
tstest test/api/endpoints.test.ts --verbose --timeout 60

Changelog

Version 1.10.0

  • ⏱️ Added --timeout <seconds> option for test file timeout protection
  • 🎯 Added --startFrom <n> and --stopAt <n> options for test file range control
  • 📁 Enhanced --logfile with intelligent log organization:
    • Previous logs moved to previous/ folder
    • Failed tests copied to 00err/ folder
    • Changed tests create diff reports in 00diff/ folder
  • 🔍 Improved test discovery to show skipped files with clear reasons
  • 🐛 Fixed TypeScript compilation warnings and unused variables
  • 📊 Test summaries now include skipped file counts

Version 1.9.2

  • 🐛 Fixed test timing display issue (removed duplicate timing in output)
  • 📝 Improved internal protocol design documentation
  • 🔧 Added protocol v2 utilities for future improvements

Version 1.9.1

  • 🐛 Fixed log file naming to preserve directory structure
  • 📁 Log files now prevent collisions: test__dir__file.log

Version 1.9.0

  • 📚 Comprehensive documentation update
  • 🏗️ Embedded tapbundle for better integration
  • 🌐 Full browser compatibility

Version 1.8.0

  • 📦 Embedded tapbundle directly into tstest project
  • 🌐 Made tapbundle fully browser-compatible
  • 📸 Added snapshot testing with base64-encoded communication protocol
  • 🏷️ Introduced tag-based test filtering
  • 🔧 Enhanced test lifecycle hooks (beforeEach/afterEach)
  • 🎯 Fixed parallel test execution and grouping
  • Improved timeout and retry mechanisms
  • 🛠️ Added test fixtures for reusable test data
  • 📊 Enhanced TAP parser for better test reporting
  • 🐛 Fixed glob pattern handling in shell scripts

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.

Description
No description provided
Readme 2 MiB
Languages
TypeScript 99.7%
JavaScript 0.3%