CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-jest-matchers

A comprehensive matcher library for Jest testing framework providing assertion utilities for test expectations

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

configuration-extension.mddocs/

Configuration and Extension

Methods for extending Jest Matchers with custom matchers and configuring test behavior. These utilities allow for customization of the testing framework and assertion counting.

Capabilities

Custom Matcher Extension

expect.extend

Adds custom matchers to the expect function, allowing you to create domain-specific assertions.

/**
 * Adds custom matchers to the expect function
 * @param matchers - Object containing custom matcher functions
 */
expect.extend(matchers: MatchersObject): void;

interface MatchersObject {
  [matcherName: string]: RawMatcherFn;
}

interface RawMatcherFn {
  (this: MatcherState, received: any, ...args: any[]): ExpectationResult;
}

interface ExpectationResult {
  pass: boolean;
  message?: string | (() => string);
}

interface MatcherState {
  isNot: boolean;
  utils: MatcherUtils;
  equals: (a: any, b: any) => boolean;
  dontThrow: () => void;
}

Usage Examples:

// Define custom matchers
expect.extend({
  toBeEven(received) {
    const pass = received % 2 === 0;
    return {
      pass,
      message: () => 
        `expected ${received} ${pass ? 'not ' : ''}to be even`
    };
  },

  toBeWithinRange(received, floor, ceiling) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        `expected ${received} ${pass ? 'not ' : ''}to be within range ${floor} - ${ceiling}`
    };
  },

  toHaveValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = typeof received === 'string' && emailRegex.test(received);
    
    return {
      pass,
      message: () =>
        `expected ${received} ${pass ? 'not ' : ''}to be a valid email address`
    };
  }
});

// Use custom matchers
expect(4).toBeEven();
expect(3).not.toBeEven();

expect(5).toBeWithinRange(1, 10);
expect(15).not.toBeWithinRange(1, 10);

expect('user@example.com').toHaveValidEmail();
expect('invalid-email').not.toHaveValidEmail();

Advanced Custom Matcher Examples:

expect.extend({
  // Async matcher
  async toEventuallyEqual(received, expected, timeout = 1000) {
    const startTime = Date.now();
    
    while (Date.now() - startTime < timeout) {
      if (this.equals(received(), expected)) {
        return {
          pass: true,
          message: () => `expected function not to eventually return ${expected}`
        };
      }
      await new Promise(resolve => setTimeout(resolve, 10));
    }
    
    return {
      pass: false,
      message: () => `expected function to eventually return ${expected} within ${timeout}ms`
    };
  },

  // Matcher with detailed diff
  toMatchUserSchema(received) {
    const requiredFields = ['id', 'name', 'email'];
    const missingFields = requiredFields.filter(field => !(field in received));
    const pass = missingFields.length === 0;
    
    return {
      pass,
      message: () => {
        if (pass) {
          return `expected user object not to match schema`;
        } else {
          return `expected user object to match schema, missing fields: ${missingFields.join(', ')}`;
        }
      }
    };
  },

  // Matcher using this.utils for formatting
  toHaveProperty(received, property, expectedValue) {
    const hasProperty = Object.prototype.hasOwnProperty.call(received, property);
    const actualValue = received[property];
    
    if (arguments.length === 2) {
      // Only check property existence
      return {
        pass: hasProperty,
        message: () =>
          `expected object ${hasProperty ? 'not ' : ''}to have property '${property}'`
      };
    } else {
      // Check property existence and value
      const pass = hasProperty && this.equals(actualValue, expectedValue);
      return {
        pass,
        message: () => {
          if (!hasProperty) {
            return `expected object to have property '${property}'`;
          } else if (!this.equals(actualValue, expectedValue)) {
            return `expected property '${property}' to be ${this.utils.printExpected(expectedValue)}, received ${this.utils.printReceived(actualValue)}`;
          } else {
            return `expected property '${property}' not to be ${this.utils.printExpected(expectedValue)}`;
          }
        }
      };
    }
  }
});

// Usage
await expect(() => getCurrentValue()).toEventuallyEqual(42, 2000);

expect({id: 1, name: 'John', email: 'john@example.com'}).toMatchUserSchema();

expect(user).toHaveProperty('name', 'John');

Assertion Counting

expect.assertions

Sets the expected number of assertions in a test. Useful for ensuring async code paths are tested.

/**
 * Sets expected number of assertions in a test
 * @param expected - Expected number of assertions to be called
 */
expect.assertions(expected: number): void;

Usage Examples:

// Ensure async callbacks are called
test('async test with assertion counting', async () => {
  expect.assertions(2);
  
  const callback = jest.fn((result) => {
    expect(result).toBeDefined();
    expect(result.success).toBe(true);
  });
  
  await processDataAsync(data, callback);
});

// Testing conditional code paths
test('conditional assertions', () => {
  expect.assertions(1);
  
  const value = getValue();
  
  if (value > 0) {
    expect(value).toBeGreaterThan(0);
  } else {
    expect(value).toBeLessThanOrEqual(0);
  }
});

// Testing promise rejections
test('promise rejection', async () => {
  expect.assertions(1);
  
  try {
    await riskyOperation();
  } catch (error) {
    expect(error.message).toContain('operation failed');
  }
});

expect.hasAssertions

Verifies that at least one assertion is called during the test. More flexible than expect.assertions().

/**
 * Verifies at least one assertion is called
 */
expect.hasAssertions(): void;

Usage Examples:

// Ensure some assertion is made in complex async flow
test('complex async flow', async () => {
  expect.hasAssertions();
  
  const results = await Promise.all([
    processItem(item1),
    processItem(item2),
    processItem(item3)
  ]);
  
  results.forEach(result => {
    if (result.shouldValidate) {
      expect(result.data).toBeDefined();
    }
  });
});

// Testing event handlers
test('event handler', () => {
  expect.hasAssertions();
  
  const handler = (event) => {
    if (event.type === 'error') {
      expect(event.message).toBeDefined();
    } else if (event.type === 'success') {
      expect(event.data).toBeDefined();
    }
  };
  
  emitter.emit('success', {data: 'test'});
});

State Management

expect.getState

Gets the current global matcher state, including assertion counts and custom state.

/**
 * Gets current global matcher state
 * @returns Current matcher state object
 */
expect.getState(): Object;

Usage Examples:

// Check current assertion count
const state = expect.getState();
console.log(`Assertions so far: ${state.assertionCalls}`);

// Access custom state in matchers
expect.extend({
  toTrackCalls(received) {
    const state = expect.getState();
    console.log(`This is assertion #${state.assertionCalls + 1}`);
    
    return {
      pass: true,
      message: () => 'tracking call'
    };
  }
});

expect.setState

Sets or updates the global matcher state. Useful for sharing data between custom matchers.

/**
 * Sets global matcher state
 * @param state - State object to merge with current state
 */
expect.setState(state: Object): void;

Usage Examples:

// Set up shared state for custom matchers
expect.setState({
  testStartTime: Date.now(),
  customConfig: {
    strictMode: true,
    debugMode: false
  }
});

// Custom matcher using shared state
expect.extend({
  toCompleteWithinTime(received, maxDuration) {
    const { testStartTime } = expect.getState();
    const actualDuration = Date.now() - testStartTime;
    const pass = actualDuration <= maxDuration;
    
    return {
      pass,
      message: () =>
        `expected test to complete within ${maxDuration}ms, took ${actualDuration}ms`
    };
  },

  toRespectStrictMode(received) {
    const { customConfig } = expect.getState();
    
    if (!customConfig?.strictMode) {
      return { pass: true, message: () => 'strict mode disabled' };
    }
    
    // Perform strict validation
    const pass = validateStrict(received);
    return {
      pass,
      message: () => `expected ${received} to pass strict validation`
    };
  }
});

// Usage
expect(result).toCompleteWithinTime(1000);
expect(data).toRespectStrictMode();

Snapshot Serializer Support

expect.addSnapshotSerializer

No-op function for compatibility with Jest's snapshot serializer system.

/**
 * No-op function for snapshot serializer compatibility
 */
expect.addSnapshotSerializer(): void;

Usage Examples:

// This is a compatibility function - it does nothing in jest-matchers
expect.addSnapshotSerializer();

// In full Jest environment, you would use it like:
// expect.addSnapshotSerializer({
//   serialize: (val) => val.toString(),
//   test: (val) => val instanceof MyClass
// });

Advanced Configuration Patterns

Setting Up Custom Matcher Suites

// Create reusable matcher suites
const customMatchers = {
  // Date matchers
  toBeToday(received) {
    const today = new Date().toDateString();
    const receivedDate = new Date(received).toDateString();
    const pass = today === receivedDate;
    
    return {
      pass,
      message: () => `expected ${received} ${pass ? 'not ' : ''}to be today`
    };
  },

  // HTTP status matchers
  toBeSuccessStatus(received) {
    const pass = received >= 200 && received < 300;
    return {
      pass,
      message: () => `expected status ${received} ${pass ? 'not ' : ''}to be successful (2xx)`
    };
  },

  // Object structure matchers
  toHaveRequiredFields(received, requiredFields) {
    const missingFields = requiredFields.filter(field => !(field in received));
    const pass = missingFields.length === 0;
    
    return {
      pass,
      message: () => {
        if (pass) {
          return `expected object not to have all required fields`;
        } else {
          return `expected object to have required fields: ${missingFields.join(', ')} missing`;
        }
      }
    };
  }
};

// Apply all custom matchers
expect.extend(customMatchers);

// Usage
expect(new Date()).toBeToday();
expect(200).toBeSuccessStatus();
expect(user).toHaveRequiredFields(['id', 'name', 'email']);

Global Test Configuration

// Set up global test state
beforeEach(() => {
  expect.setState({
    testStartTime: Date.now(),
    testId: Math.random().toString(36),
    config: {
      timeout: 5000,
      retries: 3
    }
  });
});

// Custom matchers that use global config
expect.extend({
  toCompleteWithinTimeout(received) {
    const { config, testStartTime } = expect.getState();
    const duration = Date.now() - testStartTime;
    const pass = duration <= config.timeout;
    
    return {
      pass,
      message: () => 
        `expected operation to complete within ${config.timeout}ms, took ${duration}ms`
    };
  }
});

docs

asymmetric-matchers.md

collection-string-matchers.md

configuration-extension.md

core-matchers.md

exception-matchers.md

index.md

numeric-matchers.md

spy-mock-matchers.md

tile.json