Files
gh-boneskull-claude-plugins…/skills/bupkis-assertion-patterns/references/common_patterns.md
2025-11-29 18:01:33 +08:00

604 lines
13 KiB
Markdown

# 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