668 lines
18 KiB
Markdown
668 lines
18 KiB
Markdown
# @gitzone/tstest
|
|
🧪 **A powerful, modern test runner for TypeScript** - making your test runs beautiful and informative!
|
|
|
|
## Availabililty and Links
|
|
* [npmjs.org (npm package)](https://www.npmjs.com/package/@gitzone/tstest)
|
|
* [code.foss.global (source)](https://code.foss.global/gitzone/tstest)
|
|
|
|
## 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
|
|
|
|
```bash
|
|
npm install --save-dev @gitzone/tstest
|
|
# or with pnpm
|
|
pnpm add -D @gitzone/tstest
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic Test Execution
|
|
|
|
```bash
|
|
# 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 to `.nogit/testlogs/[testname].log` |
|
|
| `--tags <tags>` | Run only tests with specific tags (comma-separated) |
|
|
|
|
### 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:
|
|
```json
|
|
{"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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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()
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
```bash
|
|
# 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.
|
|
|
|
### Automatic Logging
|
|
|
|
Use `--logfile` to automatically save test output:
|
|
```bash
|
|
tstest test/ --logfile
|
|
```
|
|
|
|
This creates detailed logs in `.nogit/testlogs/[testname].log` for each test file.
|
|
|
|
### 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:
|
|
```bash
|
|
# GitHub Actions example
|
|
tstest test/ --json > test-results.json
|
|
|
|
# Or minimal output
|
|
tstest test/ --quiet
|
|
```
|
|
|
|
## Changelog
|
|
|
|
### 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
|
|
|
|
## 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](license.md) 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. |