Expressive, readable, framework-agnostic BDD-style assertion library for JavaScript testing.
—
Methods for testing async functions, promise states, and asynchronous assertion chaining.
Test that a value is a Promise.
/**
* Assert that the value is a Promise
* @returns This assertion for chaining
*/
Promise(): Assertion;Usage:
import should from 'should';
// Basic Promise detection
Promise.resolve('value').should.be.a.Promise();
Promise.reject('error').should.be.a.Promise();
async function asyncFunc() {
return 'result';
}
asyncFunc().should.be.a.Promise();
// Function that returns a Promise
function createPromise() {
return new Promise((resolve) => {
setTimeout(() => resolve('done'), 100);
});
}
createPromise().should.be.a.Promise();
// Not a Promise
'string'.should.not.be.a.Promise();
123.should.not.be.a.Promise();
{}.should.not.be.a.Promise();Test that a Promise resolves successfully.
/**
* Assert that the Promise resolves (fulfills) successfully
* @returns Promise<any> for async testing
*/
fulfilled(): Promise<any>;
resolved(): Promise<any>;Usage:
// Basic resolution testing
Promise.resolve('success').should.be.fulfilled();
Promise.resolve(42).should.be.resolved();
// Async function testing
async function successfulOperation() {
await new Promise(resolve => setTimeout(resolve, 10));
return 'completed';
}
successfulOperation().should.be.fulfilled();
// Testing with await
await Promise.resolve('value').should.be.fulfilled();
// Chain with other assertions using .finally
Promise.resolve('hello')
.should.be.fulfilled()
.finally.equal('hello');Test that a Promise rejects.
/**
* Assert that the Promise rejects
* @returns Promise<any> for async testing
*/
rejected(): Promise<any>;Usage:
// Basic rejection testing
Promise.reject(new Error('failed')).should.be.rejected();
// Async function that throws
async function failingOperation() {
throw new Error('Operation failed');
}
failingOperation().should.be.rejected();
// Testing with await
await Promise.reject('error').should.be.rejected();
// Network request simulation
async function fetchData(url) {
if (!url) {
throw new Error('URL is required');
}
// ... fetch logic
}
fetchData().should.be.rejected();Test that a Promise resolves with a specific value.
/**
* Assert that the Promise resolves with the specified value
* @param expected - The expected resolved value
* @returns Promise<any> for async testing
*/
fulfilledWith(expected: any): Promise<any>;
resolvedWith(expected: any): Promise<any>;Usage:
// Test specific resolved values
Promise.resolve('success').should.be.fulfilledWith('success');
Promise.resolve(42).should.be.resolvedWith(42);
// Object resolution
const user = { id: 1, name: 'john' };
Promise.resolve(user).should.be.fulfilledWith(user);
// Async function result testing
async function getUserById(id) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 10));
return { id, name: `user_${id}` };
}
getUserById(123).should.be.fulfilledWith({ id: 123, name: 'user_123' });
// Array results
async function getUsers() {
return [
{ id: 1, name: 'john' },
{ id: 2, name: 'jane' }
];
}
getUsers().should.be.fulfilledWith([
{ id: 1, name: 'john' },
{ id: 2, name: 'jane' }
]);
// Testing with await
const result = await Promise.resolve('test').should.be.fulfilledWith('test');Test that a Promise rejects with a specific error or message.
/**
* Assert that the Promise rejects with the specified error/message
* @param error - Expected error (RegExp, string, Error, or Function)
* @param properties - Optional expected error properties
* @returns Promise<any> for async testing
*/
rejectedWith(error: RegExp | string | Error, properties?: object): Promise<any>;
rejectedWith(properties: object): Promise<any>;Usage:
// Test specific error messages
Promise.reject(new Error('Not found')).should.be.rejectedWith('Not found');
// Test with RegExp
Promise.reject(new Error('User ID 123 not found'))
.should.be.rejectedWith(/User ID \d+ not found/);
// Test specific error types
Promise.reject(new TypeError('Invalid input'))
.should.be.rejectedWith(TypeError);
// Test error properties
const customError = new Error('Database error');
customError.code = 500;
customError.table = 'users';
Promise.reject(customError).should.be.rejectedWith('Database error', {
code: 500,
table: 'users'
});
// Async function error testing
async function validateUser(user) {
if (!user.email) {
const error = new Error('Email is required');
error.field = 'email';
throw error;
}
}
validateUser({}).should.be.rejectedWith('Email is required', { field: 'email' });
// API error simulation
async function apiCall(endpoint) {
if (endpoint === '/forbidden') {
const error = new Error('Access denied');
error.statusCode = 403;
throw error;
}
}
apiCall('/forbidden').should.be.rejectedWith({ statusCode: 403 });Chain assertions after promise resolution.
/**
* Chain assertions after promise settles (resolved or rejected)
*/
finally: PromisedAssertion;Usage:
// Chain assertions on resolved value
Promise.resolve('hello world')
.should.be.fulfilled()
.finally.be.a.String()
.and.have.length(11)
.and.startWith('hello');
// Chain with object properties
Promise.resolve({ id: 123, name: 'john', active: true })
.should.be.fulfilled()
.finally.have.property('id', 123)
.and.have.property('name')
.which.is.a.String()
.and.have.property('active', true);
// Chain with array assertions
Promise.resolve([1, 2, 3, 4, 5])
.should.be.fulfilled()
.finally.be.an.Array()
.and.have.length(5)
.and.containEql(3);
// Complex chaining
async function fetchUserProfile(userId) {
return {
id: userId,
profile: {
name: 'John Doe',
email: 'john@example.com',
preferences: { theme: 'dark' }
},
roles: ['user', 'member']
};
}
fetchUserProfile(123)
.should.be.fulfilled()
.finally.have.property('profile')
.which.has.property('preferences')
.which.has.property('theme', 'dark');Alias for finally - chain assertions after promise settlement.
/**
* Chain assertions after promise settles - alias for finally
*/
eventually: PromisedAssertion;Usage:
// Same functionality as finally
Promise.resolve(42)
.should.be.fulfilled()
.eventually.be.a.Number()
.and.be.above(40);
// Object property testing
Promise.resolve({ count: 5, items: ['a', 'b', 'c', 'd', 'e'] })
.should.be.fulfilled()
.eventually.have.property('count', 5)
.and.have.property('items')
.which.has.length(5);class APIClient {
async get(endpoint) {
if (endpoint === '/users/404') {
const error = new Error('User not found');
error.statusCode = 404;
throw error;
}
if (endpoint === '/users/1') {
return {
id: 1,
name: 'John Doe',
email: 'john@example.com',
createdAt: '2023-01-01T00:00:00Z'
};
}
return [];
}
async post(endpoint, data) {
if (!data.name) {
const error = new Error('Name is required');
error.field = 'name';
error.statusCode = 400;
throw error;
}
return {
id: Date.now(),
...data,
createdAt: new Date().toISOString()
};
}
}
const client = new APIClient();
// Test successful API calls
client.get('/users/1')
.should.be.fulfilled()
.finally.have.properties('id', 'name', 'email')
.and.have.property('id', 1);
// Test API errors
client.get('/users/404')
.should.be.rejectedWith('User not found', { statusCode: 404 });
client.post('/users', {})
.should.be.rejectedWith('Name is required', {
field: 'name',
statusCode: 400
});
// Test successful creation
client.post('/users', { name: 'Jane Doe', email: 'jane@example.com' })
.should.be.fulfilled()
.finally.have.property('name', 'Jane Doe')
.and.have.property('id')
.which.is.a.Number();class UserRepository {
async findById(id) {
if (typeof id !== 'number') {
throw new TypeError('User ID must be a number');
}
if (id <= 0) {
throw new RangeError('User ID must be positive');
}
if (id === 999) {
return null; // User not found
}
return {
id,
name: `User ${id}`,
active: true
};
}
async create(userData) {
if (!userData.name) {
const error = new Error('User name is required');
error.code = 'VALIDATION_ERROR';
throw error;
}
return {
id: Math.floor(Math.random() * 1000),
...userData,
createdAt: new Date(),
active: true
};
}
}
const userRepo = new UserRepository();
// Test successful queries
userRepo.findById(1)
.should.be.fulfilled()
.finally.have.properties('id', 'name', 'active')
.and.have.property('active', true);
// Test not found scenario
userRepo.findById(999)
.should.be.fulfilledWith(null);
// Test validation errors
userRepo.findById('invalid')
.should.be.rejectedWith(TypeError);
userRepo.findById(-1)
.should.be.rejectedWith(RangeError);
userRepo.create({})
.should.be.rejectedWith('User name is required', { code: 'VALIDATION_ERROR' });
// Test successful creation
userRepo.create({ name: 'John Doe', email: 'john@example.com' })
.should.be.fulfilled()
.finally.have.property('name', 'John Doe')
.and.have.property('id')
.which.is.a.Number()
.and.be.above(0);const fs = require('fs').promises;
const path = require('path');
class FileManager {
async readConfig(filename) {
try {
const content = await fs.readFile(filename, 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
const notFoundError = new Error(`Config file not found: ${filename}`);
notFoundError.code = 'CONFIG_NOT_FOUND';
throw notFoundError;
}
throw error;
}
}
async writeConfig(filename, config) {
if (!config || typeof config !== 'object') {
throw new TypeError('Config must be an object');
}
const content = JSON.stringify(config, null, 2);
await fs.writeFile(filename, content, 'utf8');
return { success: true, filename };
}
}
const fileManager = new FileManager();
// Test file reading
fileManager.readConfig('existing-config.json')
.should.be.fulfilled()
.finally.be.an.Object();
// Test missing file
fileManager.readConfig('missing-config.json')
.should.be.rejectedWith(/Config file not found/, { code: 'CONFIG_NOT_FOUND' });
// Test invalid config writing
fileManager.writeConfig('test.json', null)
.should.be.rejectedWith(TypeError, 'Config must be an object');
// Test successful writing
fileManager.writeConfig('test.json', { setting: 'value' })
.should.be.fulfilled()
.finally.have.properties('success', 'filename')
.and.have.property('success', true);class RetryableOperation {
constructor(maxRetries = 3) {
this.maxRetries = maxRetries;
this.attempt = 0;
}
async execute() {
this.attempt++;
if (this.attempt <= 2) {
const error = new Error(`Attempt ${this.attempt} failed`);
error.attempt = this.attempt;
error.retryable = true;
throw error;
}
return {
success: true,
attempt: this.attempt,
message: 'Operation completed'
};
}
async executeWithTimeout(timeoutMs) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
const error = new Error('Operation timed out');
error.code = 'TIMEOUT';
reject(error);
}, timeoutMs);
this.execute()
.then(result => {
clearTimeout(timer);
resolve(result);
})
.catch(error => {
clearTimeout(timer);
reject(error);
});
});
}
}
// Test retry logic
const operation = new RetryableOperation();
// First attempts should fail
operation.execute()
.should.be.rejectedWith('Attempt 1 failed', {
attempt: 1,
retryable: true
});
// After retries, should succeed
const retryOperation = new RetryableOperation();
retryOperation.attempt = 2; // Skip to final attempt
retryOperation.execute()
.should.be.fulfilled()
.finally.have.properties('success', 'attempt', 'message')
.and.have.property('success', true);
// Test timeout
const timeoutOperation = new RetryableOperation();
timeoutOperation.executeWithTimeout(1) // Very short timeout
.should.be.rejectedWith('Operation timed out', { code: 'TIMEOUT' });const promises = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
Promise.all(promises)
.should.be.fulfilled()
.finally.eql([1, 2, 3]);
// Test Promise.all() with rejection
const mixedPromises = [
Promise.resolve('success'),
Promise.reject(new Error('failed')),
Promise.resolve('also success')
];
Promise.all(mixedPromises)
.should.be.rejectedWith('failed');const racePromises = [
new Promise(resolve => setTimeout(() => resolve('slow'), 100)),
Promise.resolve('fast')
];
Promise.race(racePromises)
.should.be.fulfilledWith('fast');// Negation examples
Promise.resolve('value').should.not.be.rejected();
Promise.reject('error').should.not.be.fulfilled();
'not a promise'.should.not.be.a.Promise();
Promise.resolve(42).should.not.be.fulfilledWith(43);
Promise.reject('wrong').should.not.be.rejectedWith('different error');
// Testing async functions that shouldn't throw
async function safeAsyncOperation() {
return 'safe result';
}
safeAsyncOperation().should.not.be.rejected();
safeAsyncOperation().should.be.fulfilledWith('safe result');Install with Tessl CLI
npx tessl i tessl/npm-should