Files
2025-11-29 18:01:33 +08:00

9.8 KiB

name, description
name description
bupkis-assertion-patterns 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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.

// ❌ 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

// Only check specific properties, ignore others
expect(result, 'to satisfy', {
  status: 'complete',
  // other properties ignored
});

Nested Structure

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

// 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

// 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