Expressive, readable, framework-agnostic BDD-style assertion library for JavaScript testing.
—
Methods for testing function exceptions, error throwing, and error property validation.
Test that a function throws an exception.
/**
* Assert that the function throws an exception
* @param message - Optional expected error message (string, RegExp, or Function)
* @param properties - Optional expected error properties
* @returns This assertion for chaining
*/
throw(): Assertion;
throw(message: RegExp | string | Function, properties?: object): Assertion;
throw(properties: object): Assertion;Usage:
import should from 'should';
// Basic exception testing
(() => {
throw new Error('Something went wrong');
}).should.throw();
// Test specific error message
(() => {
throw new Error('Invalid input');
}).should.throw('Invalid input');
// Test error message with regex
(() => {
throw new Error('User not found: ID 123');
}).should.throw(/User not found/);
// Test error type
(() => {
throw new TypeError('Expected string');
}).should.throw(TypeError);
// Test error properties
(() => {
const error = new Error('Validation failed');
error.code = 400;
error.field = 'email';
throw error;
}).should.throw({ code: 400, field: 'email' });
// Combined message and properties
(() => {
const error = new Error('Database connection failed');
error.errno = -61;
error.code = 'ECONNREFUSED';
throw error;
}).should.throw('Database connection failed', { code: 'ECONNREFUSED' });Alias for throw() with identical functionality.
/**
* Assert that the function throws an error - alias for throw()
* @param message - Optional expected error message (string, RegExp, or Function)
* @param properties - Optional expected error properties
* @returns This assertion for chaining
*/
throwError(): Assertion;
throwError(message: RegExp | string | Function, properties?: object): Assertion;
throwError(properties: object): Assertion;Usage:
// Same functionality as throw()
(() => {
throw new Error('Critical failure');
}).should.throwError();
(() => {
throw new Error('Access denied');
}).should.throwError('Access denied');
(() => {
throw new RangeError('Index out of bounds');
}).should.throwError(RangeError);// TypeError testing
(() => {
const obj = null;
obj.property; // Will throw TypeError
}).should.throw(TypeError);
// RangeError testing
(() => {
const arr = new Array(-1); // Invalid array length
}).should.throw(RangeError);
// SyntaxError testing (in eval)
(() => {
eval('invalid javascript syntax !!!');
}).should.throw(SyntaxError);
// Custom error types
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
(() => {
throw new ValidationError('Invalid email format', 'email');
}).should.throw(ValidationError);// Promise-based async functions
async function asyncFunction() {
throw new Error('Async error');
}
// Test async function (returns rejected promise)
asyncFunction().should.be.rejected();
// With specific error
async function validateUser(id) {
if (!id) {
throw new Error('User ID is required');
}
// ... more logic
}
validateUser().should.be.rejectedWith('User ID is required');function processData(data, callback) {
if (!data) {
callback(new Error('Data is required'));
return;
}
// Process data...
callback(null, processedData);
}
// Test callback error
function testCallback() {
processData(null, (err, result) => {
should.exist(err);
err.should.be.an.Error();
err.message.should.equal('Data is required');
should.not.exist(result);
});
}class APIError extends Error {
constructor(message, statusCode, details) {
super(message);
this.name = 'APIError';
this.statusCode = statusCode;
this.details = details;
}
}
function callAPI() {
throw new APIError('Request failed', 404, {
endpoint: '/users/123',
method: 'GET',
timestamp: Date.now()
});
}
// Test comprehensive error properties
(() => callAPI()).should.throw(APIError)
.and.have.property('statusCode', 404)
.and.have.property('details')
.which.has.property('endpoint');
// Alternative syntax
try {
callAPI();
should.fail('Expected APIError to be thrown');
} catch (error) {
error.should.be.instanceof(APIError);
error.should.have.property('message', 'Request failed');
error.should.have.property('statusCode', 404);
error.details.should.have.properties('endpoint', 'method');
error.details.endpoint.should.equal('/users/123');
}function validateEmail(email) {
if (typeof email !== 'string') {
throw new TypeError('Email must be a string');
}
if (!email.includes('@')) {
throw new Error('Invalid email format');
}
if (email.length < 5) {
throw new Error('Email too short');
}
}
// Test various validation scenarios
(() => validateEmail(null)).should.throw(TypeError, 'Email must be a string');
(() => validateEmail('invalid')).should.throw('Invalid email format');
(() => validateEmail('a@b')).should.throw('Email too short');
(() => validateEmail('valid@email.com')).should.not.throw();function validateAge(age) {
if (typeof age !== 'number') {
throw new TypeError('Age must be a number');
}
if (age < 0) {
throw new RangeError('Age cannot be negative');
}
if (age > 150) {
throw new RangeError('Age cannot exceed 150');
}
}
// Test age validation
(() => validateAge('25')).should.throw(TypeError);
(() => validateAge(-5)).should.throw(RangeError, 'Age cannot be negative');
(() => validateAge(200)).should.throw(RangeError, 'Age cannot exceed 150');
(() => validateAge(25)).should.not.throw();function validateConfig(config) {
if (!config) {
throw new Error('Configuration is required');
}
if (!config.apiKey) {
const error = new Error('API key is missing');
error.code = 'CONFIG_MISSING_API_KEY';
throw error;
}
if (typeof config.timeout !== 'number' || config.timeout <= 0) {
const error = new Error('Timeout must be a positive number');
error.code = 'CONFIG_INVALID_TIMEOUT';
error.received = config.timeout;
throw error;
}
}
// Test configuration validation
(() => validateConfig(null)).should.throw('Configuration is required');
(() => validateConfig({})).should.throw({ code: 'CONFIG_MISSING_API_KEY' });
(() => validateConfig({
apiKey: 'test',
timeout: -1
})).should.throw('Timeout must be a positive number', {
code: 'CONFIG_INVALID_TIMEOUT',
received: -1
});class DatabaseError extends Error {
constructor(message, query, errno) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.errno = errno;
}
}
function executeQuery(sql) {
if (!sql) {
throw new DatabaseError('SQL query is required', null, 1001);
}
if (sql.includes('DROP TABLE')) {
throw new DatabaseError('DROP TABLE not allowed', sql, 1142);
}
// ... execute query
}
// Test database error handling
(() => executeQuery('')).should.throw(DatabaseError)
.with.property('errno', 1001);
(() => executeQuery('DROP TABLE users')).should.throw(DatabaseError)
.with.property('errno', 1142)
.and.have.property('query', 'DROP TABLE users');class NetworkError extends Error {
constructor(message, statusCode, url) {
super(message);
this.name = 'NetworkError';
this.statusCode = statusCode;
this.url = url;
}
}
function fetchData(url) {
if (!url.startsWith('https://')) {
throw new NetworkError('Only HTTPS URLs allowed', 400, url);
}
if (url.includes('blocked.com')) {
throw new NetworkError('Domain is blocked', 403, url);
}
// ... fetch data
}
// Test network error scenarios
(() => fetchData('http://insecure.com')).should.throw(NetworkError)
.with.properties({
statusCode: 400,
url: 'http://insecure.com'
});
(() => fetchData('https://blocked.com/api')).should.throw(NetworkError)
.with.property('statusCode', 403);function safeOperation(data) {
// This function should not throw for valid input
return data.toString().toUpperCase();
}
function robustParser(input) {
try {
return JSON.parse(input);
} catch (e) {
return null; // Return null instead of throwing
}
}
// Test that functions don't throw
(() => safeOperation('hello')).should.not.throw();
(() => safeOperation(123)).should.not.throw();
(() => robustParser('invalid json')).should.not.throw();
// Test return values of non-throwing functions
safeOperation('hello').should.equal('HELLO');
should(robustParser('invalid json')).be.null();
should(robustParser('{"valid": true}')).eql({ valid: true });function complexValidation(user) {
const errors = [];
if (!user.name || user.name.length < 2) {
errors.push({ field: 'name', message: 'Name must be at least 2 characters' });
}
if (!user.email || !user.email.includes('@')) {
errors.push({ field: 'email', message: 'Valid email is required' });
}
if (user.age !== undefined && (user.age < 0 || user.age > 150)) {
errors.push({ field: 'age', message: 'Age must be between 0 and 150' });
}
if (errors.length > 0) {
const error = new Error('Validation failed');
error.name = 'ValidationError';
error.errors = errors;
throw error;
}
return user;
}
// Test multiple validation errors
(() => complexValidation({
name: 'A',
email: 'invalid',
age: -5
})).should.throw('Validation failed')
.with.property('errors')
.which.has.length(3);
// Test successful validation
(() => complexValidation({
name: 'John Doe',
email: 'john@example.com',
age: 30
})).should.not.throw();const throwingFunction = () => {
throw new Error('Test error');
};
const nonThrowingFunction = () => {
return 'success';
};
// Chaining with other assertions
throwingFunction.should.be.a.Function().and.throw();
nonThrowingFunction.should.be.a.Function().and.not.throw();
// Complex chaining
(() => {
const error = new Error('Custom error');
error.code = 500;
throw error;
}).should.throw()
.with.property('message', 'Custom error')
.and.have.property('code', 500);Install with Tessl CLI
npx tessl i tessl/npm-should