Simple JavaScript testing framework for browsers and node.js
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Jasmine's system for extending functionality with custom matchers, equality testers, object formatters, and spy strategies. Allows developers to create domain-specific testing utilities and enhance Jasmine's built-in capabilities.
Functions for adding custom synchronous and asynchronous matchers.
/**
* Add custom synchronous matchers to Jasmine
* @param matchers - Object mapping matcher names to factory functions
*/
jasmine.addMatchers(matchers: { [matcherName: string]: MatcherFactory }): void;
/**
* Add custom asynchronous matchers to Jasmine
* @param matchers - Object mapping matcher names to async factory functions
*/
jasmine.addAsyncMatchers(matchers: { [matcherName: string]: AsyncMatcherFactory }): void;
interface MatcherFactory {
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): Matcher;
}
interface AsyncMatcherFactory {
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): AsyncMatcher;
}
interface Matcher {
compare(actual: any, expected?: any): MatcherResult;
negativeCompare?(actual: any, expected?: any): MatcherResult;
}
interface AsyncMatcher {
compare(actual: any, expected?: any): Promise<MatcherResult>;
negativeCompare?(actual: any, expected?: any): Promise<MatcherResult>;
}
interface MatcherResult {
pass: boolean;
message?: string | (() => string);
}Usage Examples:
// Custom synchronous matcher
jasmine.addMatchers({
toBeValidEmail: (util, customEqualityTesters) => {
return {
compare: (actual, expected) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const pass = emailRegex.test(actual);
return {
pass: pass,
message: pass
? `Expected ${actual} not to be a valid email`
: `Expected ${actual} to be a valid email`
};
}
};
},
toBeWithinRange: (util, customEqualityTesters) => {
return {
compare: (actual, min, max) => {
const pass = actual >= min && actual <= max;
return {
pass: pass,
message: pass
? `Expected ${actual} not to be within range ${min}-${max}`
: `Expected ${actual} to be within range ${min}-${max}`
};
}
};
}
});
// Using custom matchers
expect('user@example.com').toBeValidEmail();
expect(50).toBeWithinRange(1, 100);
expect(150).not.toBeWithinRange(1, 100);
// Custom asynchronous matcher
jasmine.addAsyncMatchers({
toEventuallyContain: (util, customEqualityTesters) => {
return {
compare: async (actualPromise, expected) => {
try {
const actual = await actualPromise;
const pass = Array.isArray(actual) && actual.includes(expected);
return {
pass: pass,
message: pass
? `Expected array not to eventually contain ${expected}`
: `Expected array to eventually contain ${expected}`
};
} catch (error) {
return {
pass: false,
message: `Promise rejected: ${error.message}`
};
}
}
};
}
});
// Using async matcher
await expectAsync(fetchUserList()).toEventuallyContain('Alice');Functions for adding custom equality comparison logic.
/**
* Add a custom equality tester function
* @param tester - Function that tests equality between two values
*/
jasmine.addCustomEqualityTester(tester: EqualityTester): void;
interface EqualityTester {
(first: any, second: any): boolean | undefined;
}Usage Examples:
// Custom equality for objects with 'equals' method
jasmine.addCustomEqualityTester((first, second) => {
if (first && second && typeof first.equals === 'function') {
return first.equals(second);
}
// Return undefined to let Jasmine handle with default equality
return undefined;
});
// Custom equality for case-insensitive strings
jasmine.addCustomEqualityTester((first, second) => {
if (typeof first === 'string' && typeof second === 'string') {
return first.toLowerCase() === second.toLowerCase();
}
return undefined;
});
// Custom equality for arrays ignoring order
jasmine.addCustomEqualityTester((first, second) => {
if (Array.isArray(first) && Array.isArray(second)) {
if (first.length !== second.length) return false;
const sortedFirst = [...first].sort();
const sortedSecond = [...second].sort();
for (let i = 0; i < sortedFirst.length; i++) {
if (sortedFirst[i] !== sortedSecond[i]) return false;
}
return true;
}
return undefined;
});
// Usage with custom equality
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
equals(other) {
return other instanceof Person &&
this.name === other.name &&
this.age === other.age;
}
}
const person1 = new Person('Alice', 30);
const person2 = new Person('Alice', 30);
expect(person1).toEqual(person2); // Uses custom equality
expect('Hello').toEqual('HELLO'); // Case-insensitive comparison
expect([1, 2, 3]).toEqual([3, 1, 2]); // Order-independent array comparisonFunctions for customizing how objects are displayed in test output.
/**
* Add a custom object formatter for pretty-printing
* @param formatter - Function that formats objects for display
*/
jasmine.addCustomObjectFormatter(formatter: ObjectFormatter): void;
interface ObjectFormatter {
(object: any): string | undefined;
}Usage Examples:
// Custom formatter for Date objects
jasmine.addCustomObjectFormatter((object) => {
if (object instanceof Date) {
return `Date(${object.toISOString()})`;
}
return undefined; // Let Jasmine handle other types
});
// Custom formatter for User objects
class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
}
jasmine.addCustomObjectFormatter((object) => {
if (object instanceof User) {
return `User{id: ${object.id}, name: "${object.name}", email: "${object.email}"}`;
}
return undefined;
});
// Custom formatter for large arrays
jasmine.addCustomObjectFormatter((object) => {
if (Array.isArray(object) && object.length > 10) {
return `Array[${object.length}] [${object.slice(0, 3).join(', ')}, ..., ${object.slice(-2).join(', ')}]`;
}
return undefined;
});
// These will now use custom formatting in error messages
const user = new User(1, 'Alice', 'alice@example.com');
const largeArray = new Array(100).fill(0).map((_, i) => i);
expect(user.name).toBe('Bob'); // Error shows User{...} format
expect(largeArray).toContain(200); // Error shows Array[100] [...] formatFunctions for adding custom spy behavior strategies.
/**
* Add a custom spy strategy that can be used by any spy
* @param name - Name of the strategy
* @param factory - Function that creates the strategy behavior
*/
jasmine.addSpyStrategy(name: string, factory: SpyStrategyFactory): void;
/**
* Set the default spy strategy for all new spies
* @param defaultStrategyFn - Function that returns default strategy behavior
*/
jasmine.setDefaultSpyStrategy(defaultStrategyFn: DefaultSpyStrategyFunction): void;
interface SpyStrategyFactory {
(spy: Spy): Function;
}
interface DefaultSpyStrategyFunction {
(name: string, originalFn?: Function): Function;
}Usage Examples:
// Custom spy strategy that logs all calls
jasmine.addSpyStrategy('logCalls', (spy) => {
return function(...args) {
console.log(`Spy ${spy.identity} called with:`, args);
return undefined;
};
});
// Custom spy strategy that tracks call count
jasmine.addSpyStrategy('countCalls', (spy) => {
let callCount = 0;
return function(...args) {
callCount++;
spy.callCount = callCount;
return `Called ${callCount} times`;
};
});
// Custom spy strategy for delayed responses
jasmine.addSpyStrategy('delayedReturn', (spy) => {
return function(value, delay = 100) {
return new Promise(resolve => {
setTimeout(() => resolve(value), delay);
});
};
});
// Using custom spy strategies
const spy = jasmine.createSpy('testSpy');
spy.and.logCalls();
spy('arg1', 'arg2'); // Logs: "Spy testSpy called with: ['arg1', 'arg2']"
spy.and.countCalls();
console.log(spy()); // "Called 1 times"
console.log(spy.callCount); // 1
const asyncSpy = jasmine.createSpy('asyncSpy');
asyncSpy.and.delayedReturn('response', 200);
const result = await asyncSpy(); // Returns 'response' after 200ms
// Set default spy behavior
jasmine.setDefaultSpyStrategy((name, originalFn) => {
return function(...args) {
console.log(`Default spy ${name} intercepted call`);
return originalFn ? originalFn.apply(this, args) : undefined;
};
});
// All new spies will use the default strategy
const newSpy = jasmine.createSpy('newSpy'); // Logs when calledUtility functions and objects available to custom matchers.
interface MatchersUtil {
/**
* Test equality using Jasmine's equality logic
* @param a - First value
* @param b - Second value
* @param customTesters - Custom equality testers
* @returns Whether values are equal
*/
equals(a: any, b: any, customTesters?: EqualityTester[]): boolean;
/**
* Check if value contains expected value
* @param haystack - Container to search in
* @param needle - Value to find
* @param customTesters - Custom equality testers
* @returns Whether container contains value
*/
contains(haystack: any, needle: any, customTesters?: EqualityTester[]): boolean;
/**
* Build failure message for negative comparison
* @param matcherName - Name of the matcher
* @param isNot - Whether this is a negative assertion
* @param actual - Actual value
* @param expected - Expected arguments
* @returns Formatted error message
*/
buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: any[]): string;
}Usage Examples:
jasmine.addMatchers({
toBeArrayContainingInOrder: (util, customEqualityTesters) => {
return {
compare: (actual, ...expectedItems) => {
if (!Array.isArray(actual)) {
return {
pass: false,
message: 'Expected actual to be an array'
};
}
let actualIndex = 0;
for (const expectedItem of expectedItems) {
let found = false;
for (let i = actualIndex; i < actual.length; i++) {
if (util.equals(actual[i], expectedItem, customEqualityTesters)) {
actualIndex = i + 1;
found = true;
break;
}
}
if (!found) {
return {
pass: false,
message: util.buildFailureMessage(
'toBeArrayContainingInOrder',
false,
actual,
...expectedItems
)
};
}
}
return { pass: true };
}
};
}
});
// Usage
expect([1, 2, 3, 4, 5]).toBeArrayContainingInOrder(1, 3, 5);
expect(['a', 'b', 'c', 'd']).toBeArrayContainingInOrder('a', 'c');Guidelines and patterns for creating effective custom extensions.
// Helper function for creating consistent matcher results
function createMatcherResult(pass: boolean, actualDesc: string, expectedDesc: string, not: boolean = false): MatcherResult {
const verb = not ? 'not to' : 'to';
return {
pass: pass,
message: pass === not
? `Expected ${actualDesc} ${verb} ${expectedDesc}`
: undefined
};
}Usage Examples:
// Well-structured custom matcher with proper error messages
jasmine.addMatchers({
toHaveProperty: (util, customEqualityTesters) => {
return {
compare: (actual, propertyName, expectedValue) => {
if (actual == null) {
return {
pass: false,
message: `Expected ${actual} to have property '${propertyName}'`
};
}
const hasProperty = propertyName in actual;
if (arguments.length === 2) {
// Just checking property existence
return {
pass: hasProperty,
message: hasProperty
? `Expected object not to have property '${propertyName}'`
: `Expected object to have property '${propertyName}'`
};
} else {
// Checking property value
const hasCorrectValue = hasProperty &&
util.equals(actual[propertyName], expectedValue, customEqualityTesters);
return {
pass: hasCorrectValue,
message: () => {
if (!hasProperty) {
return `Expected object to have property '${propertyName}'`;
} else {
return `Expected property '${propertyName}' to be ${jasmine.pp(expectedValue)} but was ${jasmine.pp(actual[propertyName])}`;
}
}
};
}
}
};
}
});
// Usage
expect({ name: 'Alice', age: 30 }).toHaveProperty('name');
expect({ name: 'Alice', age: 30 }).toHaveProperty('age', 30);
expect({ name: 'Alice', age: 30 }).not.toHaveProperty('email');interface MatcherFactory {
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): Matcher;
}
interface AsyncMatcherFactory {
(util: MatchersUtil, customEqualityTesters: EqualityTester[]): AsyncMatcher;
}
interface Matcher {
compare(actual: any, expected?: any): MatcherResult;
negativeCompare?(actual: any, expected?: any): MatcherResult;
}
interface AsyncMatcher {
compare(actual: any, expected?: any): Promise<MatcherResult>;
negativeCompare?(actual: any, expected?: any): Promise<MatcherResult>;
}
interface MatcherResult {
pass: boolean;
message?: string | (() => string);
}
interface EqualityTester {
(first: any, second: any): boolean | undefined;
}
interface ObjectFormatter {
(object: any): string | undefined;
}
interface SpyStrategyFactory {
(spy: Spy): Function;
}
interface DefaultSpyStrategyFunction {
(name: string, originalFn?: Function): Function;
}
interface MatchersUtil {
equals(a: any, b: any, customTesters?: EqualityTester[]): boolean;
contains(haystack: any, needle: any, customTesters?: EqualityTester[]): boolean;
buildFailureMessage(matcherName: string, isNot: boolean, actual: any, ...expected: any[]): string;
}