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

error-assertions.mddocs/

Error and Exception Assertions

Methods for testing function exceptions, error throwing, and error property validation.

Exception Testing

throw()

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' });

throwError()

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);

Specific Error Types

Testing Different Error Types

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

Async Function Testing

Testing Async Functions That Throw

// 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');

Testing Callback-based Functions

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);
  });
}

Error Property Validation

Testing Custom Error Properties

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');
}

Validation Function Testing

Input Validation

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();

Range Validation

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();

Configuration Validation

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 
});

Database and Network Error Testing

Database Error Simulation

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');

Network Error Simulation

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);

Testing Functions That Should NOT Throw

Successful Execution Testing

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 });

Complex Error Scenario Testing

Multiple Error Conditions

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();

Chaining and Negation

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

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