Initial commit
This commit is contained in:
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