CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-should

Expressive, readable, framework-agnostic BDD-style assertion library for JavaScript testing.

Pending
Overview
Eval results
Files

configuration.mddocs/

Configuration and Extension

Library configuration options, prototype extension management, and custom assertion plugin system.

Library Configuration

should.config

Global configuration object that controls should.js behavior.

/**
 * Global configuration object for should.js behavior
 */
interface Config {
  /** Whether .eql checks object prototypes for equality */
  checkProtoEql: boolean;
  /** Whether .eql treats +0 and -0 as equal */
  plusZeroAndMinusZeroEqual: boolean;
  /** Additional formatting options for should-format */
  [key: string]: any;
}

const config: Config;

Usage:

import should from 'should';

// Default configuration
console.log(should.config.checkProtoEql); // false
console.log(should.config.plusZeroAndMinusZeroEqual); // true

// Modify configuration
should.config.checkProtoEql = true;
should.config.plusZeroAndMinusZeroEqual = false;

// Test behavior with different settings
const objA = { value: 10 };
const objB = Object.create(null);
objB.value = 10;

// With checkProtoEql = false (default)
should.config.checkProtoEql = false;
objA.should.eql(objB); // Passes - prototypes ignored

// With checkProtoEql = true
should.config.checkProtoEql = true;
objA.should.not.eql(objB); // Different prototypes detected

// Zero equality behavior
should.config.plusZeroAndMinusZeroEqual = true;
(+0).should.eql(-0); // Passes

should.config.plusZeroAndMinusZeroEqual = false;
(+0).should.not.eql(-0); // Fails - treats +0 and -0 as different

Prototype Extension Management

should.extend()

Extend a prototype with the should property using a custom name.

/**
 * Extend given prototype with should property using specified name
 * @param propertyName - Name of property to add (default: 'should')
 * @param proto - Prototype to extend (default: Object.prototype)
 * @returns Descriptor object to restore later with noConflict()
 */
extend(propertyName?: string, proto?: object): {
  name: string;
  descriptor: PropertyDescriptor | undefined;
  proto: object;
};

Usage:

import should from 'should';

// Default extension (already done automatically)
// Object.prototype.should = getter

// Custom property name
const mustDescriptor = should.extend('must', Object.prototype);
'test'.must.be.a.String(); // Now using 'must' instead of 'should'

// Custom prototype
class CustomClass {
  constructor(value) {
    this.value = value;
  }
}

const customDescriptor = should.extend('expect', CustomClass.prototype);
const instance = new CustomClass(42);
instance.expect.have.property('value', 42);

// Multiple extensions
const checkDescriptor = should.extend('check', Array.prototype);
[1, 2, 3].check.have.length(3);

// Store descriptors for later cleanup
const extensions = {
  must: should.extend('must'),
  expect: should.extend('expect', CustomClass.prototype),
  check: should.extend('check', Array.prototype)
};

should.noConflict()

Remove should extension and restore previous property if it existed.

/**
 * Remove should extension and restore previous property descriptor
 * @param descriptor - Descriptor returned from extend() (optional)
 * @returns The should function
 */
noConflict(descriptor?: {
  name: string;
  descriptor: PropertyDescriptor | undefined;
  proto: object;
}): typeof should;

Usage:

// Remove default should extension
const cleanShould = should.noConflict();

// Now Object.prototype.should is removed
try {
  'test'.should.be.a.String(); // TypeError: Cannot read property 'should'
} catch (error) {
  console.log('should property removed');
}

// Use functional syntax instead
cleanShould('test').should.be.a.String(); // Works

// Remove specific extension
const mustDescriptor = should.extend('must');
'test'.must.be.a.String(); // Works

should.noConflict(mustDescriptor);
// Now 'must' property is removed

// Restore previous property if it existed
Object.prototype.previousProperty = 'original';
const prevDescriptor = should.extend('previousProperty');
'test'.previousProperty.be.a.String(); // should assertion

should.noConflict(prevDescriptor);
console.log('test'.previousProperty); // 'original' - restored

Functional API Usage

As-Function Import

Use should.js without prototype extension.

/**
 * Should.js as a function without prototype extension
 * Import from 'should/as-function' to avoid Object.prototype modification
 */
declare function should(obj: any): should.Assertion;

Usage:

// Import as function only
const should = require('should/as-function');
// or
import should from 'should/as-function';

// No prototype extension - Object.prototype.should doesn't exist
console.log(typeof ''.should); // 'undefined'

// Use functional syntax
should('test').be.a.String();
should(42).be.a.Number().and.be.above(0);
should({ name: 'john' }).have.property('name', 'john');

// Useful for testing null/undefined values
should(null).be.null();
should(undefined).be.undefined();

// Can still use all assertion methods
should([1, 2, 3])
  .be.an.Array()
  .and.have.length(3)
  .and.containEql(2);

Plugin System

should.use()

Add custom assertion plugins to extend functionality.

/**
 * Use a plugin to extend should.js functionality
 * @param plugin - Plugin function that receives (should, Assertion) parameters
 * @returns The should function for chaining
 */
use(plugin: (should: any, Assertion: any) => void): typeof should;

Usage:

// Basic plugin example
should.use(function(should, Assertion) {
  // Add custom assertion method
  Assertion.add('between', function(min, max) {
    this.params = { 
      operator: `to be between ${min} and ${max}` 
    };
    
    this.obj.should.be.a.Number();
    this.obj.should.be.above(min - 1);
    this.obj.should.be.below(max + 1);
  });
});

// Use the custom assertion
(5).should.be.between(1, 10);
(15).should.not.be.between(1, 10);

// Advanced plugin with multiple methods
should.use(function(should, Assertion) {
  // Email validation assertion
  Assertion.add('email', function() {
    this.params = { operator: 'to be a valid email' };
    
    this.obj.should.be.a.String();
    this.obj.should.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
  
  // Phone number validation
  Assertion.add('phoneNumber', function() {
    this.params = { operator: 'to be a valid phone number' };
    
    this.obj.should.be.a.String();
    this.obj.should.match(/^\(?[\d\s\-\+\(\)]{10,}$/);
  });
  
  // Credit card validation (simplified)
  Assertion.add('creditCard', function() {
    this.params = { operator: 'to be a valid credit card number' };
    
    this.obj.should.be.a.String();
    this.obj.should.match(/^\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}$/);
  });
});

// Use custom validation assertions
'user@example.com'.should.be.email();
'123-456-7890'.should.be.phoneNumber();
'1234 5678 9012 3456'.should.be.creditCard();

Complex Plugin Examples

HTTP Status Code Plugin

should.use(function(should, Assertion) {
  // HTTP status code categories
  const statusCategories = {
    informational: (code) => code >= 100 && code < 200,
    success: (code) => code >= 200 && code < 300,
    redirection: (code) => code >= 300 && code < 400,
    clientError: (code) => code >= 400 && code < 500,
    serverError: (code) => code >= 500 && code < 600
  };
  
  Object.keys(statusCategories).forEach(category => {
    Assertion.add(category, function() {
      this.params = { operator: `to be a ${category} status code` };
      
      this.obj.should.be.a.Number();
      statusCategories[category](this.obj).should.be.true();
    });
  });
  
  // Specific status codes
  const specificCodes = {
    ok: 200,
    created: 201,
    badRequest: 400,
    unauthorized: 401,
    forbidden: 403,
    notFound: 404,
    internalServerError: 500
  };
  
  Object.keys(specificCodes).forEach(name => {
    Assertion.add(name, function() {
      this.params = { operator: `to be ${name} (${specificCodes[name]})` };
      this.obj.should.equal(specificCodes[name]);
    });
  });
});

// Usage
(200).should.be.success();
(404).should.be.clientError();
(500).should.be.serverError();

(200).should.be.ok();
(404).should.be.notFound();
(401).should.be.unauthorized();

Date Range Plugin

should.use(function(should, Assertion) {
  Assertion.add('dateAfter', function(date) {
    this.params = { operator: `to be after ${date}` };
    
    this.obj.should.be.a.Date();
    this.obj.getTime().should.be.above(new Date(date).getTime());
  });
  
  Assertion.add('dateBefore', function(date) {
    this.params = { operator: `to be before ${date}` };
    
    this.obj.should.be.a.Date();
    this.obj.getTime().should.be.below(new Date(date).getTime());
  });
  
  Assertion.add('dateWithin', function(startDate, endDate) {
    this.params = { 
      operator: `to be between ${startDate} and ${endDate}` 
    };
    
    this.obj.should.be.a.Date();
    const time = this.obj.getTime();
    const start = new Date(startDate).getTime();
    const end = new Date(endDate).getTime();
    
    time.should.be.above(start - 1);
    time.should.be.below(end + 1);
  });
  
  Assertion.add('today', function() {
    this.params = { operator: 'to be today' };
    
    this.obj.should.be.a.Date();
    const today = new Date();
    const objDate = new Date(this.obj);
    
    objDate.getFullYear().should.equal(today.getFullYear());
    objDate.getMonth().should.equal(today.getMonth());
    objDate.getDate().should.equal(today.getDate());
  });
});

// Usage
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
const now = new Date();

now.should.be.dateAfter(yesterday);
now.should.be.dateBefore(tomorrow);
now.should.be.dateWithin(yesterday, tomorrow);
now.should.be.today();

API Response Plugin

should.use(function(should, Assertion) {
  Assertion.add('apiResponse', function() {
    this.params = { operator: 'to be a valid API response' };
    
    this.obj.should.be.an.Object();
    this.obj.should.have.properties('status', 'data');
    this.obj.status.should.be.a.Number();
  });
  
  Assertion.add('successResponse', function() {
    this.params = { operator: 'to be a successful API response' };
    
    this.obj.should.be.apiResponse();
    this.obj.status.should.be.within(200, 299);
  });
  
  Assertion.add('errorResponse', function() {
    this.params = { operator: 'to be an error API response' };
    
    this.obj.should.be.apiResponse();
    this.obj.status.should.be.above(399);
    this.obj.should.have.property('error');
  });
  
  Assertion.add('paginatedResponse', function() {
    this.params = { operator: 'to be a paginated API response' };
    
    this.obj.should.be.successResponse();
    this.obj.should.have.property('pagination');
    this.obj.pagination.should.have.properties('page', 'limit', 'total');
  });
});

// Usage
const response = {
  status: 200,
  data: [{ id: 1, name: 'user1' }],
  pagination: { page: 1, limit: 10, total: 50 }
};

response.should.be.apiResponse();
response.should.be.successResponse();
response.should.be.paginatedResponse();

const errorResponse = {
  status: 404,
  data: null,
  error: { message: 'Not found' }
};

errorResponse.should.be.errorResponse();

Advanced Configuration

Environment-Specific Setup

// Development environment
if (process.env.NODE_ENV === 'development') {
  should.config.checkProtoEql = true; // Strict prototype checking
  
  // Add development-only assertions
  should.use(function(should, Assertion) {
    Assertion.add('debug', function() {
      console.log('Debug assertion:', this.obj);
      return this;
    });
  });
}

// Test environment
if (process.env.NODE_ENV === 'test') {
  // More lenient configuration for tests
  should.config.plusZeroAndMinusZeroEqual = true;
  
  // Test-specific utilities
  should.use(function(should, Assertion) {
    Assertion.add('testFixture', function(type) {
      this.params = { operator: `to be a ${type} test fixture` };
      
      this.obj.should.be.an.Object();
      this.obj.should.have.property('_fixture', type);
    });
  });
}

Framework Integration

// Express.js response testing
should.use(function(should, Assertion) {
  Assertion.add('expressResponse', function() {
    this.params = { operator: 'to be an Express response object' };
    
    this.obj.should.be.an.Object();
    this.obj.should.have.properties('status', 'json', 'send');
    this.obj.status.should.be.a.Function();
  });
});

// React component testing
should.use(function(should, Assertion) {
  Assertion.add('reactComponent', function() {
    this.params = { operator: 'to be a React component' };
    
    this.obj.should.be.a.Function();
    // Additional React-specific checks
  });
});

Chaining and Integration

All configuration and extension features integrate with standard should.js chaining:

// Chain configuration changes
should.config.checkProtoEql = true;

const obj1 = { value: 1 };
const obj2 = Object.create(null);
obj2.value = 1;

obj1.should.not.eql(obj2); // Prototype difference detected

// Chain custom assertions
'user@test.com'.should.be.email().and.be.a.String();
(404).should.be.clientError().and.be.notFound();

// Use extensions in complex chains
const apiResponse = { status: 200, data: [] };
apiResponse.should.be.successResponse()
  .and.have.property('data')
  .which.is.an.Array();

Install with Tessl CLI

npx tessl i tessl/npm-should

docs

basic-assertions.md

configuration.md

containment-assertions.md

error-assertions.md

index.md

number-assertions.md

pattern-matching.md

promise-assertions.md

property-assertions.md

string-assertions.md

type-assertions.md

tile.json