13 KiB
13 KiB
Common Bupkis Patterns
Practical patterns and real-world examples for using Bupkis assertions effectively.
Table of Contents
- API Response Validation
- Configuration Validation
- Error Testing Patterns
- Async Operation Patterns
- Complex Nested Structures
- Test Data Validation
- Type Safety Patterns
API Response Validation
REST API Success Response
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
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
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
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
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
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
// 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
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
// 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
// ❌ 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
// 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
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
// 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
// Too brittle - breaks if order changes
expect(users, 'to deep equal', [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
]);
✅ Do: Use Partial Matching
// 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
// 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
// Object check already ensures non-null
expect(config, 'to be an object');
❌ Don't: Multiple Separate Assertions
// 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
// 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
- Use
to satisfyfor complex objects - It's more maintainable than multiple assertions - Leverage
expect.it()for nested validation - Embed assertions within object patterns - Use RegExp for flexible string matching - More robust than exact string comparisons
- Chain related assertions with 'and' - Keeps related checks together
- Prefer semantic assertions - Use
to have propertyinstead of'key' in obj - Use
to have propertiesfor multiple keys - More concise than chaining - Let assertions imply related checks - Don't redundantly check for null after object check
- Use
expectAsyncfor promises - Makes contract explicit: resolve or reject