Initial commit
This commit is contained in:
307
skills/bupkis-assertion-patterns/SKILL.md
Normal file
307
skills/bupkis-assertion-patterns/SKILL.md
Normal file
@@ -0,0 +1,307 @@
|
||||
---
|
||||
name: bupkis-assertion-patterns
|
||||
description: How to write idiomatic assertions with the Bupkis assertion library for TypeScript and JavaScript
|
||||
---
|
||||
|
||||
# Bupkis Assertion Patterns
|
||||
|
||||
Write idiomatic, expressive assertions using bupkis' powerful assertion vocabulary to make tests more readable and maintainable.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Writing tests with the bupkis assertion library
|
||||
- Checking properties, types, or structure of objects
|
||||
- Verifying arrays or collections
|
||||
- Want clearer, more expressive test failures
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- When a simple, direct assertion is clearer (don't over-complicate)
|
||||
- When the pattern doesn't improve readability
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Use semantic assertions**: Choose assertions that express intent, not implementation
|
||||
2. **Combine related checks**: Use `to satisfy` for multiple properties rather than separate assertions
|
||||
3. **Leverage bupkis vocabulary**: Use built-in assertions like `to have property`, `to be empty`, etc.
|
||||
4. **Let assertions imply related checks**: e.g., `to be an object` already implies non-null
|
||||
|
||||
## Patterns
|
||||
|
||||
### 1. Property Existence
|
||||
|
||||
**Prefer semantic property checks over truthiness checks.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - unclear intent, indirect check
|
||||
expect('filesCompleted' in state, 'to be truthy');
|
||||
|
||||
// ✅ DO - clear, semantic assertion
|
||||
expect(state, 'to have property', 'filesCompleted');
|
||||
```
|
||||
|
||||
**Why:** `to have property` expresses the intent clearly and provides better error messages.
|
||||
|
||||
---
|
||||
|
||||
### 2. Type Checking Multiple Properties
|
||||
|
||||
**Combine related type checks using `to satisfy` instead of separate assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - repetitive, verbose
|
||||
expect(typeof state.filesCompleted, 'to equal', 'number');
|
||||
expect(typeof state.suitesCompleted, 'to equal', 'number');
|
||||
expect(typeof state.tasksCompleted, 'to equal', 'number');
|
||||
|
||||
// ✅ DO - concise, shows structure at a glance
|
||||
expect(state, 'to satisfy', {
|
||||
filesCompleted: expect.it('to be a number'),
|
||||
suitesCompleted: expect.it('to be a number'),
|
||||
tasksCompleted: expect.it('to be a number'),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** `to satisfy` lets you verify multiple properties in one assertion, showing the expected structure clearly. Better error messages show exactly which properties failed.
|
||||
|
||||
---
|
||||
|
||||
### 3. Non-Empty Collections
|
||||
|
||||
**Use semantic collection assertions instead of length comparisons.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - indirect, requires mental math
|
||||
expect(result.files.length, 'to be greater than', 0);
|
||||
|
||||
// ✅ DO - direct, semantic
|
||||
expect(result.files, 'not to be empty');
|
||||
```
|
||||
|
||||
**Why:** `not to be empty` directly expresses the intent. Works for arrays, strings, objects, Maps, Sets, etc.
|
||||
|
||||
---
|
||||
|
||||
### 4. Object and Null Checks
|
||||
|
||||
**Don't redundantly check for null when object check already implies it.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - redundant null check
|
||||
expect(config, 'to be an object');
|
||||
expect(config, 'not to be null');
|
||||
|
||||
// ✅ DO - object check implies non-null
|
||||
expect(config, 'to be an object'); // already implies non-null
|
||||
```
|
||||
|
||||
**Why:** In bupkis, `to be an object` already ensures the value is not null. Redundant checks add noise.
|
||||
|
||||
---
|
||||
|
||||
### 5. Object Structure Verification
|
||||
|
||||
**Verify object structure with `to satisfy` instead of multiple property assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - fragmented, hard to see expected structure
|
||||
expect(config, 'to be an object');
|
||||
expect(config.iterations, 'to equal', 500);
|
||||
expect(config.reporters[0], 'to equal', 'json');
|
||||
expect(config.reporters.length, 'to equal', 1);
|
||||
|
||||
// ✅ DO - clear, declarative structure check
|
||||
expect(config, 'to satisfy', {
|
||||
iterations: 500,
|
||||
reporters: expect.it('to deep equal', ['json']),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** `to satisfy` lets you declaratively specify the expected structure. Combines type check and property validation in one assertion. Shows the expected shape at a glance.
|
||||
|
||||
---
|
||||
|
||||
### 5b. Multiple Conditions on the Same Result Object
|
||||
|
||||
**Combine multiple checks on a result object with `to satisfy` instead of separate assertions.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - multiple separate assertions
|
||||
expect(result.exitCode, 'to equal', 0);
|
||||
expect(result.stdout, 'to match', /No historical data/);
|
||||
expect(result.stderr, 'not to match', /toLocaleDateString is not a function/);
|
||||
|
||||
// ✅ DO - single to satisfy assertion
|
||||
expect(result, 'to satisfy', {
|
||||
exitCode: 0,
|
||||
stdout: expect.it('to match', /No historical data/),
|
||||
stderr: expect.it('not to match', /toLocaleDateString is not a function/),
|
||||
});
|
||||
```
|
||||
|
||||
**Why:** Groups all related checks on the same object. Shows the expected result state clearly. Easier to maintain - add/remove checks in one place. Better error messages show exactly which property failed.
|
||||
|
||||
---
|
||||
|
||||
### 6. Defined Value Checks
|
||||
|
||||
**Use positive assertions instead of negated ones when possible.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - negated assertion
|
||||
expect(result, 'not to be undefined');
|
||||
|
||||
// ✅ DO - positive, semantic
|
||||
expect(result, 'to be defined');
|
||||
```
|
||||
|
||||
**Why:** `to be defined` is clearer and more idiomatic than negating undefined. It's a positive assertion that directly expresses intent.
|
||||
|
||||
---
|
||||
|
||||
### 7. Chaining Assertions with 'and'
|
||||
|
||||
**Use concatenation when making multiple assertions on the same subject.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - separate assertions
|
||||
expect(config, 'to be an object'); // implies non-null
|
||||
expect(config, 'to have property', 'reporters');
|
||||
|
||||
// ✅ DO - chain with 'and'
|
||||
expect(config, 'to be an object', 'and', 'to have property', 'reporters');
|
||||
```
|
||||
|
||||
**Why:** Chaining assertions with `'and'` keeps related checks together in a single statement. More concise and shows that you're checking multiple aspects of the same value. Better error messages that show which part of the chain failed.
|
||||
|
||||
---
|
||||
|
||||
### 8. Multiple Property Checks with Array
|
||||
|
||||
**Use `to have properties` with an array when checking for multiple properties.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - chain multiple property checks
|
||||
expect(
|
||||
config,
|
||||
'to be an object',
|
||||
'and',
|
||||
'to have property',
|
||||
'outputDir',
|
||||
'and',
|
||||
'to have property',
|
||||
'reporters',
|
||||
);
|
||||
|
||||
// ✅ DO - use 'to have properties' with array
|
||||
expect(config, 'to have properties', ['outputDir', 'reporters']);
|
||||
```
|
||||
|
||||
**Why:** `to have properties` with an array is specifically designed for checking multiple properties at once. More concise than chaining. Shows all required properties clearly in one place. Better error messages that list all missing properties.
|
||||
|
||||
---
|
||||
|
||||
### 9. Promise Rejection Checks
|
||||
|
||||
**Use `expectAsync` with `'to reject'` for testing promise rejections.**
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T - wishy-washy try/catch
|
||||
try {
|
||||
await configManager.load('nonexistent.config.json');
|
||||
expect(true, 'to be truthy'); // Maybe it works?
|
||||
} catch (error) {
|
||||
expect(error, 'to be an', Error); // Or maybe it throws?
|
||||
expect((error as Error).message, 'not to be empty');
|
||||
}
|
||||
|
||||
// ✅ DO - explicit promise rejection check
|
||||
await expectAsync(configManager.load('nonexistent.config.json'), 'to reject');
|
||||
```
|
||||
|
||||
**Why:** Makes the contract explicit - either it should reject or it shouldn't. No ambiguity. `expectAsync` is specifically designed for promise-based assertions. `'to reject'` clearly expresses that rejection is the expected behavior.
|
||||
|
||||
**Related assertions:**
|
||||
|
||||
- `'to reject'` - promise should be rejected
|
||||
- `'to reject with error satisfying'` - promise should reject with specific error
|
||||
|
||||
---
|
||||
|
||||
## Advanced `to satisfy` Patterns
|
||||
|
||||
### Partial Object Matching
|
||||
|
||||
```typescript
|
||||
// Only check specific properties, ignore others
|
||||
expect(result, 'to satisfy', {
|
||||
status: 'complete',
|
||||
// other properties ignored
|
||||
});
|
||||
```
|
||||
|
||||
### Nested Structure
|
||||
|
||||
```typescript
|
||||
expect(benchmark, 'to satisfy', {
|
||||
name: expect.it('to be a string'),
|
||||
results: expect.it('to satisfy', {
|
||||
mean: expect.it('to be a number'),
|
||||
median: expect.it('to be a number'),
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Array Element Values
|
||||
|
||||
```typescript
|
||||
// Check specific array values within to satisfy
|
||||
expect(config, 'to satisfy', {
|
||||
iterations: 100,
|
||||
reporters: ['human'], // checks first element matches
|
||||
});
|
||||
|
||||
// Or check multiple elements
|
||||
expect(config, 'to satisfy', {
|
||||
tags: ['performance', 'critical'],
|
||||
});
|
||||
```
|
||||
|
||||
### Arrays with Patterns
|
||||
|
||||
```typescript
|
||||
// All items must satisfy condition
|
||||
expect(results, 'to have items satisfying', {
|
||||
duration: expect.it('to be a number'),
|
||||
status: 'success',
|
||||
});
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Instead of... | Use... |
|
||||
| -------------------------------------- | ------------------------------------------------------ |
|
||||
| `'prop' in obj, 'to be truthy'` | `obj, 'to have property', 'prop'` |
|
||||
| `typeof x.prop, 'to equal', 'number'` | `x, 'to satisfy', {prop: expect.it('to be a number')}` |
|
||||
| `arr.length, 'to be greater than', 0` | `arr, 'not to be empty'` |
|
||||
| `result, 'not to be undefined'` | `result, 'to be defined'` |
|
||||
| Separate expect() on same subject | Chain with `'and'`: `expect(x, 'a', 'and', 'b')` |
|
||||
| Multiple 'to have property' assertions | `to have properties`, `['prop1', 'prop2']` |
|
||||
| try/catch for promise rejection | `await expectAsync(promise, 'to reject')` |
|
||||
| Multiple assertions on the same object | `to satisfy` with object structure |
|
||||
| Separate object + null checks | Just `to be an object` |
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clearer intent**: Semantic assertions express what you're testing, not how
|
||||
2. **Better error messages**: bupkis shows exactly what failed in structural checks
|
||||
3. **More maintainable**: Related checks grouped together, easier to update
|
||||
4. **Less code**: Combine multiple assertions into expressive structure checks
|
||||
5. **Discover structure**: `to satisfy` shows expected object shape at a glance
|
||||
|
||||
## Tools Used
|
||||
|
||||
- `expect()` with bupkis assertion vocabulary
|
||||
- `expect.it()` for nested assertions within `to satisfy`
|
||||
- Semantic assertions: `to have property`, `not to be empty`, `to be an object`
|
||||
- Structural assertions: `to satisfy`, `to deep equal`
|
||||
69
skills/bupkis-assertion-patterns/references/README.md
Normal file
69
skills/bupkis-assertion-patterns/references/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Bupkis References
|
||||
|
||||
This directory contains reference documentation for the Bupkis assertion library.
|
||||
|
||||
## Available References
|
||||
|
||||
### 1. api_reference.md - Complete API Documentation
|
||||
|
||||
**Comprehensive reference for all built-in Bupkis assertions** organized by category:
|
||||
|
||||
- **Core Concepts** - Natural language assertions, negation, concatenation, embeddable assertions
|
||||
- **Primitive Assertions** - Type checks (string, number, boolean, null, undefined, etc.)
|
||||
- **Numeric Assertions** - Number comparisons, ranges, special values (NaN, Infinity)
|
||||
- **String & Pattern Assertions** - String matching, RegExp, substring operations
|
||||
- **Collection Assertions** - Arrays, Maps, Sets, WeakMap, WeakSet operations
|
||||
- **Object Assertions** - Property checks, object matching, `to satisfy` patterns
|
||||
- **Function Assertions** - Function type checks, arity, throw behavior
|
||||
- **Equality & Comparison Assertions** - Deep equality, instance checks
|
||||
- **Error Assertions** - Error type and message validation
|
||||
- **Date & Time Assertions** - Date comparisons, durations, weekday/weekend checks
|
||||
- **Promise Assertions** - Async resolution and rejection testing
|
||||
- **Other Assertions** - Truthy, falsy, defined checks
|
||||
|
||||
**When to use:** When you need to look up the exact syntax for an assertion, understand what parameters it accepts, or see all available aliases.
|
||||
|
||||
### 2. common_patterns.md - Practical Usage Patterns
|
||||
|
||||
**Real-world examples and best practices** for common testing scenarios:
|
||||
|
||||
- **API Response Validation** - REST API, pagination, error responses
|
||||
- **Configuration Validation** - App config, environment-specific config, feature flags
|
||||
- **Error Testing Patterns** - Standard errors, custom errors, promise rejections
|
||||
- **Async Operation Patterns** - Promise resolution, async functions, race conditions
|
||||
- **Complex Nested Structures** - Deep object validation, arrays of objects, optional properties
|
||||
- **Test Data Validation** - Factory data, fixtures, mock data consistency
|
||||
- **Type Safety Patterns** - Discriminated unions, generic validation, branded types
|
||||
- **Real-World Scenarios** - Database queries, forms, file system, HTTP headers, events
|
||||
|
||||
**When to use:** When implementing tests and need examples of idiomatic Bupkis patterns for common use cases.
|
||||
|
||||
## How These References Are Used
|
||||
|
||||
When Claude loads this skill and you ask about Bupkis assertions, it will:
|
||||
|
||||
1. **Load SKILL.md first** (always in context) - Provides the core patterns and workflow
|
||||
2. **Load references as needed** - Fetches specific reference files based on your question:
|
||||
- Questions about "what assertions are available" → `api_reference.md`
|
||||
- Questions about "how to test API responses" → `common_patterns.md`
|
||||
- Questions about specific assertion syntax → `api_reference.md`
|
||||
|
||||
This three-level progressive disclosure keeps context usage efficient while ensuring comprehensive information is available when needed.
|
||||
|
||||
## Reference File Organization
|
||||
|
||||
Both reference files use:
|
||||
|
||||
- Clear table of contents for quick navigation
|
||||
- Consistent formatting (heading hierarchy, code blocks)
|
||||
- Success ✓ and failure ✗ examples for each assertion
|
||||
- Inline comments explaining key concepts
|
||||
- Cross-references between related sections
|
||||
|
||||
## Extending These References
|
||||
|
||||
If you discover new patterns or need to document additional use cases:
|
||||
|
||||
1. **Add patterns to common_patterns.md** - Keep examples practical and well-commented
|
||||
2. **Update api_reference.md** - If Bupkis adds new assertions
|
||||
3. **Keep files under 10k words each** - Split into subtopics if growing too large
|
||||
1408
skills/bupkis-assertion-patterns/references/api_reference.md
Normal file
1408
skills/bupkis-assertion-patterns/references/api_reference.md
Normal file
File diff suppressed because it is too large
Load Diff
603
skills/bupkis-assertion-patterns/references/common_patterns.md
Normal file
603
skills/bupkis-assertion-patterns/references/common_patterns.md
Normal file
@@ -0,0 +1,603 @@
|
||||
# Common Bupkis Patterns
|
||||
|
||||
Practical patterns and real-world examples for using Bupkis assertions effectively.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [API Response Validation](#api-response-validation)
|
||||
- [Configuration Validation](#configuration-validation)
|
||||
- [Error Testing Patterns](#error-testing-patterns)
|
||||
- [Async Operation Patterns](#async-operation-patterns)
|
||||
- [Complex Nested Structures](#complex-nested-structures)
|
||||
- [Test Data Validation](#test-data-validation)
|
||||
- [Type Safety Patterns](#type-safety-patterns)
|
||||
|
||||
---
|
||||
|
||||
## API Response Validation
|
||||
|
||||
### REST API Success Response
|
||||
|
||||
```typescript
|
||||
const response = await fetch('/api/users/1');
|
||||
const data = await response.json();
|
||||
|
||||
// Validate response structure
|
||||
expect(data, 'to satisfy', {
|
||||
id: expect.it('to be a number'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
name: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
createdAt: expect.it('to be a valid date'),
|
||||
roles: expect.it('to be an array', 'and', 'not to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
### Paginated API Response
|
||||
|
||||
```typescript
|
||||
const response = await fetchPaginatedUsers({ page: 1, limit: 10 });
|
||||
|
||||
expect(response, 'to satisfy', {
|
||||
data: expect.it('to be an array', 'and', 'to have length', 10),
|
||||
meta: {
|
||||
currentPage: 1,
|
||||
totalPages: expect.it('to be a positive integer'),
|
||||
totalItems: expect.it('to be a positive integer'),
|
||||
hasNextPage: expect.it('to be a boolean'),
|
||||
},
|
||||
});
|
||||
|
||||
// Validate each user in the array
|
||||
expect(response.data, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### Error Response Validation
|
||||
|
||||
```typescript
|
||||
const response = await fetchWithError('/api/invalid');
|
||||
|
||||
expect(response, 'to satisfy', {
|
||||
error: {
|
||||
code: expect.it('to be a string'),
|
||||
message: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
statusCode: expect.it('to be between', 400, 599),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
### Application Config
|
||||
|
||||
```typescript
|
||||
const config = loadAppConfig();
|
||||
|
||||
expect(config, 'to satisfy', {
|
||||
port: expect.it('to be between', 1024, 65535),
|
||||
host: expect.it('to be a string'),
|
||||
database: {
|
||||
url: /^postgresql:\/\//,
|
||||
poolSize: expect.it('to be a positive integer'),
|
||||
},
|
||||
logging: {
|
||||
level: expect.it('to be one of', ['debug', 'info', 'warn', 'error']),
|
||||
format: expect.it('to be one of', ['json', 'text']),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Environment-Specific Config
|
||||
|
||||
```typescript
|
||||
const prodConfig = loadConfig('production');
|
||||
|
||||
expect(prodConfig, 'to satisfy', {
|
||||
env: 'production',
|
||||
debug: false,
|
||||
apiUrl: /^https:\/\//, // Must be HTTPS in production
|
||||
rateLimiting: {
|
||||
enabled: true,
|
||||
maxRequests: expect.it('to be a positive integer'),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Flags
|
||||
|
||||
```typescript
|
||||
const featureFlags = getFeatureFlags();
|
||||
|
||||
expect(featureFlags, 'to be an object', 'and', 'not to be empty');
|
||||
|
||||
expect(featureFlags, 'to satisfy', {
|
||||
newDashboard: expect.it('to be a boolean'),
|
||||
betaFeatures: expect.it('to be a boolean'),
|
||||
experimentalApi: expect.it('to be a boolean'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Testing Patterns
|
||||
|
||||
### Standard Error Handling
|
||||
|
||||
```typescript
|
||||
// Function that should throw
|
||||
expect(() => parseJSON('invalid json'), 'to throw a', SyntaxError);
|
||||
|
||||
// With message matching
|
||||
expect(
|
||||
() => parseJSON('invalid json'),
|
||||
'to throw a',
|
||||
SyntaxError,
|
||||
'satisfying',
|
||||
{ message: /Unexpected token/ },
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Error Properties
|
||||
|
||||
```typescript
|
||||
expect(
|
||||
() => {
|
||||
const err = new Error('Database connection failed');
|
||||
err.code = 'ECONNREFUSED';
|
||||
err.port = 5432;
|
||||
throw err;
|
||||
},
|
||||
'to throw an error satisfying',
|
||||
{
|
||||
message: /connection failed/,
|
||||
code: 'ECONNREFUSED',
|
||||
port: 5432,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Promise Rejection
|
||||
|
||||
```typescript
|
||||
// Simple rejection
|
||||
await expectAsync(fetchData('/api/invalid'), 'to reject');
|
||||
|
||||
// With error type
|
||||
await expectAsync(fetchData('/api/invalid'), 'to reject with a', HttpError);
|
||||
|
||||
// With error properties
|
||||
await expectAsync(
|
||||
fetchData('/api/invalid'),
|
||||
'to reject with error satisfying',
|
||||
{
|
||||
statusCode: 404,
|
||||
message: /not found/,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Try-Catch Alternative
|
||||
|
||||
```typescript
|
||||
// ❌ DON'T use try-catch for expected errors
|
||||
try {
|
||||
await riskyOperation();
|
||||
expect(true, 'to be truthy'); // Maybe it works?
|
||||
} catch (error) {
|
||||
expect(error, 'to be an', Error); // Or maybe it throws?
|
||||
}
|
||||
|
||||
// ✅ DO use expectAsync with explicit contract
|
||||
await expectAsync(riskyOperation(), 'to reject with a', ValidationError);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Operation Patterns
|
||||
|
||||
### Promise Resolution
|
||||
|
||||
```typescript
|
||||
// Basic resolution
|
||||
await expectAsync(fetchUser(123), 'to resolve');
|
||||
|
||||
// With value validation
|
||||
await expectAsync(fetchUser(123), 'to resolve with value satisfying', {
|
||||
id: 123,
|
||||
name: expect.it('to be a string'),
|
||||
email: /^[^\s@]+@/,
|
||||
});
|
||||
```
|
||||
|
||||
### Async Function Testing
|
||||
|
||||
```typescript
|
||||
async function processData(input) {
|
||||
// ... processing
|
||||
return result;
|
||||
}
|
||||
|
||||
await expectAsync(
|
||||
async () => processData({ valid: true }),
|
||||
'to resolve with value satisfying',
|
||||
{ success: true },
|
||||
);
|
||||
|
||||
await expectAsync(
|
||||
async () => processData({ invalid: true }),
|
||||
'to reject with error satisfying',
|
||||
{ code: 'INVALID_INPUT' },
|
||||
);
|
||||
```
|
||||
|
||||
### Race Conditions
|
||||
|
||||
```typescript
|
||||
// Ensure operation completes within timeout
|
||||
const promise = Promise.race([slowOperation(), timeout(5000)]);
|
||||
|
||||
await expectAsync(promise, 'to resolve with value satisfying', {
|
||||
completed: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complex Nested Structures
|
||||
|
||||
### Deep Object Validation
|
||||
|
||||
```typescript
|
||||
const benchmark = {
|
||||
name: 'Array.sort performance',
|
||||
date: '2024-01-15',
|
||||
results: {
|
||||
samples: [1.2, 1.3, 1.1, 1.4, 1.2],
|
||||
statistics: {
|
||||
mean: 1.24,
|
||||
median: 1.2,
|
||||
stdDev: 0.11,
|
||||
min: 1.1,
|
||||
max: 1.4,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
platform: 'Node.js v20.10.0',
|
||||
arch: 'x64',
|
||||
},
|
||||
};
|
||||
|
||||
expect(benchmark, 'to satisfy', {
|
||||
name: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
date: /^\d{4}-\d{2}-\d{2}$/,
|
||||
results: {
|
||||
samples: expect.it('to be an array', 'and', 'to be non-empty'),
|
||||
statistics: {
|
||||
mean: expect.it('to be a number', 'and', 'to be positive'),
|
||||
median: expect.it('to be a number', 'and', 'to be positive'),
|
||||
stdDev: expect.it('to be a number'),
|
||||
min: expect.it('to be a number'),
|
||||
max: expect.it('to be a number'),
|
||||
},
|
||||
},
|
||||
metadata: expect.it('to be an object'),
|
||||
});
|
||||
```
|
||||
|
||||
### Array of Complex Objects
|
||||
|
||||
```typescript
|
||||
const transactions = [
|
||||
{ id: 1, amount: 100.5, currency: 'USD', status: 'completed' },
|
||||
{ id: 2, amount: 250.0, currency: 'EUR', status: 'pending' },
|
||||
{ id: 3, amount: 75.25, currency: 'GBP', status: 'completed' },
|
||||
];
|
||||
|
||||
// Validate array structure
|
||||
expect(transactions, 'to be an array', 'and', 'not to be empty');
|
||||
|
||||
// Validate each transaction
|
||||
expect(transactions, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
amount: expect.it('to be a positive number'),
|
||||
currency: expect.it('to be one of', ['USD', 'EUR', 'GBP']),
|
||||
status: expect.it('to be one of', ['pending', 'completed', 'failed']),
|
||||
});
|
||||
```
|
||||
|
||||
### Optional Properties
|
||||
|
||||
```typescript
|
||||
const user = {
|
||||
id: 123,
|
||||
name: 'Alice',
|
||||
email: 'alice@example.com',
|
||||
// phone is optional
|
||||
};
|
||||
|
||||
// Use `to satisfy` to ignore missing optional properties
|
||||
expect(user, 'to satisfy', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
name: expect.it('to be a string'),
|
||||
email: /^[^\s@]+@/,
|
||||
// phone not required, will pass if missing
|
||||
});
|
||||
|
||||
// Explicitly check for optional property if present
|
||||
if ('phone' in user) {
|
||||
expect(user.phone, 'to match', /^\+?\d{10,15}$/);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Data Validation
|
||||
|
||||
### Factory-Generated Data
|
||||
|
||||
```typescript
|
||||
const user = UserFactory.create();
|
||||
|
||||
// Validate factory output
|
||||
expect(user, 'to satisfy', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
createdAt: expect.it('to be a Date'),
|
||||
roles: expect.it('to be an array', 'and', 'not to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
### Fixture Data
|
||||
|
||||
```typescript
|
||||
const fixtures = loadFixtures('users.json');
|
||||
|
||||
expect(fixtures, 'to be an array', 'and', 'to be non-empty');
|
||||
|
||||
// Validate all fixtures
|
||||
expect(fixtures, 'to have items satisfying', {
|
||||
id: expect.it('to be a number'),
|
||||
email: expect.it('to be a string'),
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### Mock Data Consistency
|
||||
|
||||
```typescript
|
||||
const mockResponse = {
|
||||
users: [
|
||||
/* ... */
|
||||
],
|
||||
meta: { total: 50 },
|
||||
};
|
||||
|
||||
// Ensure mock data is consistent
|
||||
expect(
|
||||
mockResponse.users.length,
|
||||
'to be less than or equal to',
|
||||
mockResponse.meta.total,
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Safety Patterns
|
||||
|
||||
### Discriminated Unions
|
||||
|
||||
```typescript
|
||||
type SuccessResult = { success: true; data: any };
|
||||
type ErrorResult = { success: false; error: string };
|
||||
type Result = SuccessResult | ErrorResult;
|
||||
|
||||
const result: Result = processOperation();
|
||||
|
||||
if (result.success) {
|
||||
expect(result, 'to satisfy', {
|
||||
success: true,
|
||||
data: expect.it('to be defined'),
|
||||
});
|
||||
} else {
|
||||
expect(result, 'to satisfy', {
|
||||
success: false,
|
||||
error: expect.it('to be a string', 'and', 'not to be empty'),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Generic Type Validation
|
||||
|
||||
```typescript
|
||||
function validateResponse<T>(
|
||||
response: unknown,
|
||||
shape: Record<string, any>,
|
||||
): asserts response is T {
|
||||
expect(response, 'to satisfy', shape);
|
||||
}
|
||||
|
||||
// Usage
|
||||
const response = await fetchData();
|
||||
validateResponse<User>(response, {
|
||||
id: expect.it('to be a number'),
|
||||
email: expect.it('to be a string'),
|
||||
});
|
||||
|
||||
// TypeScript now knows response is User
|
||||
console.log(response.email);
|
||||
```
|
||||
|
||||
### Branded Types
|
||||
|
||||
```typescript
|
||||
type UserId = number & { __brand: 'UserId' };
|
||||
|
||||
function assertUserId(value: number): asserts value is UserId {
|
||||
expect(value, 'to be a positive integer');
|
||||
}
|
||||
|
||||
const id: number = 123;
|
||||
assertUserId(id);
|
||||
// id is now UserId
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Scenarios
|
||||
|
||||
### Database Query Results
|
||||
|
||||
```typescript
|
||||
const users = await db.query('SELECT * FROM users WHERE active = true');
|
||||
|
||||
expect(users, 'to be an array');
|
||||
expect(users, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
email: expect.it('to be a string'),
|
||||
active: true,
|
||||
created_at: expect.it('to be a Date'),
|
||||
});
|
||||
```
|
||||
|
||||
### Form Validation
|
||||
|
||||
```typescript
|
||||
const formData = {
|
||||
username: 'alice123',
|
||||
email: 'alice@example.com',
|
||||
password: 'securePassword123!',
|
||||
age: 25,
|
||||
};
|
||||
|
||||
expect(formData, 'to satisfy', {
|
||||
username: expect.it('to be a string', 'and', 'to have length between', 3, 20),
|
||||
email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
||||
password: expect.it('to be a string', 'and', 'to have length at least', 8),
|
||||
age: expect.it('to be between', 13, 120),
|
||||
});
|
||||
```
|
||||
|
||||
### File System Operations
|
||||
|
||||
```typescript
|
||||
const stats = await fs.stat('package.json');
|
||||
|
||||
expect(stats, 'to satisfy', {
|
||||
size: expect.it('to be a positive number'),
|
||||
isFile: expect.it('to be a function'),
|
||||
isDirectory: expect.it('to be a function'),
|
||||
mtime: expect.it('to be a Date'),
|
||||
});
|
||||
|
||||
expect(stats.isFile(), 'to be true');
|
||||
```
|
||||
|
||||
### HTTP Headers Validation
|
||||
|
||||
```typescript
|
||||
const headers = response.headers;
|
||||
|
||||
expect(headers, 'to satisfy', {
|
||||
'content-type': /^application\/json/,
|
||||
'cache-control': expect.it('to be a string'),
|
||||
'x-request-id': expect.it('to be a string', 'and', 'not to be empty'),
|
||||
});
|
||||
|
||||
// Check CORS headers
|
||||
expect(headers, 'to have property', 'access-control-allow-origin');
|
||||
```
|
||||
|
||||
### Event Emission Validation
|
||||
|
||||
```typescript
|
||||
const emitter = new EventEmitter();
|
||||
const events: any[] = [];
|
||||
|
||||
emitter.on('data', (data) => events.push(data));
|
||||
|
||||
emitter.emit('data', { id: 1, value: 'test' });
|
||||
emitter.emit('data', { id: 2, value: 'test2' });
|
||||
|
||||
expect(events, 'to have length', 2);
|
||||
expect(events, 'to have items satisfying', {
|
||||
id: expect.it('to be a positive integer'),
|
||||
value: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Don't: Overly Specific Assertions
|
||||
|
||||
```typescript
|
||||
// Too brittle - breaks if order changes
|
||||
expect(users, 'to deep equal', [
|
||||
{ id: 1, name: 'Alice' },
|
||||
{ id: 2, name: 'Bob' },
|
||||
]);
|
||||
```
|
||||
|
||||
### ✅ Do: Use Partial Matching
|
||||
|
||||
```typescript
|
||||
// More flexible - only checks what matters
|
||||
expect(users, 'to have items satisfying', {
|
||||
id: expect.it('to be a number'),
|
||||
name: expect.it('to be a string'),
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Don't: Redundant Checks
|
||||
|
||||
```typescript
|
||||
// Redundant - `to be an object` already implies non-null
|
||||
expect(config, 'to be an object');
|
||||
expect(config, 'not to be null'); // Unnecessary!
|
||||
```
|
||||
|
||||
### ✅ Do: Rely on Implied Checks
|
||||
|
||||
```typescript
|
||||
// Object check already ensures non-null
|
||||
expect(config, 'to be an object');
|
||||
```
|
||||
|
||||
### ❌ Don't: Multiple Separate Assertions
|
||||
|
||||
```typescript
|
||||
// Fragmented - hard to see the expected structure
|
||||
expect(result.exitCode, 'to equal', 0);
|
||||
expect(result.stdout, 'to match', /success/);
|
||||
expect(result.stderr, 'to be empty');
|
||||
```
|
||||
|
||||
### ✅ Do: Use `to satisfy` for Related Properties
|
||||
|
||||
```typescript
|
||||
// Clear - shows expected structure at a glance
|
||||
expect(result, 'to satisfy', {
|
||||
exitCode: 0,
|
||||
stdout: /success/,
|
||||
stderr: expect.it('to be empty'),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips for Writing Better Assertions
|
||||
|
||||
1. **Use `to satisfy` for complex objects** - It's more maintainable than multiple assertions
|
||||
2. **Leverage `expect.it()` for nested validation** - Embed assertions within object patterns
|
||||
3. **Use RegExp for flexible string matching** - More robust than exact string comparisons
|
||||
4. **Chain related assertions with 'and'** - Keeps related checks together
|
||||
5. **Prefer semantic assertions** - Use `to have property` instead of `'key' in obj`
|
||||
6. **Use `to have properties` for multiple keys** - More concise than chaining
|
||||
7. **Let assertions imply related checks** - Don't redundantly check for null after object check
|
||||
8. **Use `expectAsync` for promises** - Makes contract explicit: resolve or reject
|
||||
Reference in New Issue
Block a user