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

promise-assertions.mddocs/

Promise Assertions

Methods for testing async functions, promise states, and asynchronous assertion chaining.

Promise State Testing

Promise()

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

Promise Resolution Testing

fulfilled() / resolved()

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

rejected()

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

Promise Value Testing

fulfilledWith() / resolvedWith()

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

rejectedWith()

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

Async Assertion Chaining

finally

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

eventually

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

Practical Async Testing Examples

API Testing

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

Database Operations

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

File Operations

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

Timeout and Retry Logic

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

Advanced Promise Testing

Promise.all() Testing

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

Promise.race() Testing

const racePromises = [
  new Promise(resolve => setTimeout(() => resolve('slow'), 100)),
  Promise.resolve('fast')
];

Promise.race(racePromises)
  .should.be.fulfilledWith('fast');

Negation and Error Cases

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

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