Expressive, readable, framework-agnostic BDD-style assertion library for JavaScript testing.
—
Library configuration options, prototype extension management, and custom assertion plugin system.
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 differentExtend 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)
};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' - restoredUse 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);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();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();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();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();// 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);
});
});
}// 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
});
});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