A comprehensive matcher library for Jest testing framework providing assertion utilities for test expectations
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Methods for extending Jest Matchers with custom matchers and configuring test behavior. These utilities allow for customization of the testing framework and assertion counting.
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');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');
}
});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'});
});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'
};
}
});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();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
// });// 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']);// 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`
};
}
});