0.23.1 โข Published 9 months ago
@corez/mocker v0.23.1
@corez/mocker
A powerful TypeScript mocking library with flexible configuration and behavior control.
Features
- ๐ Fully type-safe API with TypeScript
- ๐ญ Function, object and class mocking
- ๐ช Flexible mock configuration
- ๐ฒ Call tracking and verification
- ๐พ Instance isolation and state management
- ๐ฏ Getter/Setter mocking support
- ๐ Original implementation call-through
- ๐ฆ Zero runtime dependencies
- ๐ Comprehensive error handling
- ๐งช Async function support
- ๐จ Deep object comparison
- ๐ง Chainable API
Table of Contents
Installation
# Using pnpm (recommended)
pnpm add -D @corez/mocker
# Using npm
npm install --save-dev @corez/mocker
# Using yarn
yarn add -D @corez/mockerRequirements
- Node.js 16.0.0 or later
- TypeScript 5.0.0 or later
Quick Start
import {mocker} from '@corez/mocker';
// Mock a simple function
const fn = mocker.fn();
fn.mockReturnValue(42);
console.log(fn()); // 42
// Mock an object with call tracking
const user = {getName: () => 'John'};
const mockedUser = mocker.object(user);
mockedUser.getName.mockReturnValue('Jane');
console.log(mockedUser.getName()); // Jane
console.log(mockedUser.getName.mock.calls.length); // 1
// Mock a class with instance tracking
class UserService {
getUser(id: number) {
return {id, name: 'John'};
}
}
const MockedService = mocker.class(UserService);
const instance = new MockedService();
instance.getUser.mockReturnValue({id: 1, name: 'Jane'});
console.log(MockedService.mock.instances.length); // 1Basic Usage
Function Mocking
// Basic mock function
const fn = mocker.fn();
// With implementation
const fn = mocker.fn((x: number) => x * 2);
// Return values
fn.mockReturnValue(42);
fn.mockReturnValueOnce(1);
fn.mockReturnValueOnce(2).mockReturnValue(3);
// Implementations
fn.mockImplementation(x => x * 2);
fn.mockImplementationOnce(x => x + 1);
// Async functions
const asyncFn = mocker.fn(async () => 'result');
asyncFn.mockResolvedValue('mocked');
asyncFn.mockRejectedValue(new Error('failed'));
// Reset and verification
fn.mockClear(); // Clear calls
fn.mockReset(); // Clear calls and implementationObject Mocking
interface User {
id: number;
name: string;
getFullName(): string;
}
const user: User = {
id: 1,
name: 'John',
getFullName: () => 'John Doe',
};
// Mock with type safety
const mockedUser = mocker.object<User>(user);
// Mock methods
mockedUser.getFullName.mockReturnValue('Jane Doe');
// Call through to original implementation
const mockedWithCallThrough = mocker.object(user, {callThrough: true});
console.log(mockedWithCallThrough.getFullName()); // 'John Doe'
// Reset mocks
mockedUser.mockReset();Class Mocking
class UserService {
private api: ApiClient;
constructor(api: ApiClient) {
this.api = api;
}
async getUser(id: number): Promise<User> {
return this.api.fetch(`/users/${id}`);
}
}
// Mock entire class
const MockedService = mocker.class(UserService);
const instance = new MockedService();
// Mock specific method
instance.getUser.mockResolvedValue({id: 1, name: 'John'});
// Custom implementation
MockedService.mockImplementation(
class extends UserService {
async getUser(id: number) {
return {id, name: 'Custom'};
}
},
);
// Reset class
MockedService.mockReset();Instance Mocking
class User {
constructor(public name: string) {}
getName() {
return this.name;
}
}
// Create mocked instance with overrides
const mockedUser = mocker.instance(User, {
name: 'Jane',
getName: () => 'Jane Doe',
});
// Mock existing instance
const user = new User('John');
const mockedExisting = mocker.instance(user);Advanced Usage
Call Through Behavior
class Calculator {
add(a: number, b: number) {
return a + b;
}
}
// Mock with call through
const mockedCalc = mocker.object(new Calculator(), {
callThrough: true,
});
// Calls original implementation
console.log(mockedCalc.add(2, 3)); // 5
// Still tracks calls
console.log(mockedCalc.add.mock.calls); // [[2, 3]]Keeping Original Methods
const obj = {
method1() {
return 1;
},
method2() {
return 2;
},
};
// Keep specific methods original
const mocked = mocker.object(obj, {
keepOriginal: ({property}) => property === 'method1',
});
// method1 remains original, method2 is mocked
console.log(mocked.method1()); // 1
console.log(mocked.method2()); // undefinedAssertions
@corez/mocker provides a powerful assertion API with various matching patterns and detailed error reporting.
Basic Assertions
// Call count assertions
assert.called(spy); // Called at least once
assert.notCalled(spy); // Never called
assert.calledOnce(spy); // Called exactly once
assert.calledTwice(spy); // Called exactly twice
assert.calledThrice(spy); // Called exactly three times
assert.callCount(spy, n); // Called n times
// Argument assertions
assert.calledWith(spy, ...args); // Called with matching arguments
assert.calledWithExactly(spy, ...args); // Called with exact arguments
assert.calledOnceWith(spy, ...args); // Called once with matching argumentsMatchers
@corez/mocker supports three types of matchers:
- Built-in Matchers
import {matchers} from '@corez/mocker';
// Type matching
assert.calledWith(
spy,
matchers.string(), // Match any string
matchers.number(), // Match any number
matchers.boolean(), // Match any boolean
matchers.any(), // Match any value
);
// Array matching
assert.calledWith(
spy,
matchers.arrayContaining([1, 2]), // Array contains specified elements
);
// Object matching
assert.calledWith(
spy,
matchers.objectContaining({
// Object contains specified properties
id: matchers.number(),
name: matchers.string(),
}),
);- Jest-style Matchers
assert.calledWith(spy,
expect.any(Number), // Type matching
expect.stringMatching(/test/), // RegExp matching
expect.objectContaining({...}), // Object containing
expect.arrayContaining([...]) // Array containing
);- Custom Matchers
assert.calledWith(spy, {
asymmetricMatch: actual => actual.startsWith('test'),
});Deep Matching
Support for nested objects and arrays deep matching:
assert.calledWith(spy, {
user: {
id: matchers.number(),
profile: {
name: matchers.string(),
tags: matchers.arrayContaining(['admin']),
meta: matchers.objectContaining({
verified: matchers.boolean(),
}),
},
},
});Error Assertions
Multiple error matching patterns:
// String matching
assert.threw(spy, 'error message');
// RegExp matching
assert.threw(spy, /error/);
// Constructor matching
assert.threw(spy, TypeError);
// Property matching
assert.threw(spy, {
message: 'error message',
code: 'ERR_001',
});
// Custom matching
assert.threw(spy, {
asymmetricMatch: error => error.code === 'ERR_001',
});Context Assertions
// this context assertions
assert.calledOn(spy, thisValue);
// Constructor call assertions
assert.calledWithNew(spy);Error Formatting
When assertions fail, detailed error reports are generated:
// Call: spy({name: 'test', age: 25})
assert.calledWith(spy, {
name: 'other',
age: 30,
});
// Error message:
// AssertionError: expected spy to have been called with specific arguments
// Call 1:
// Expected: [{"name":"other","age":30}]
// Received: [{"name":"test","age":25}]
//
// Difference at index 0:
// Expected: {"name":"other","age":30}
// Received: {"name":"test","age":25}Best Practices
- Reset Mocks Between Tests
beforeEach(() => {
mocker.reset(mockedFunction);
mockedObject.mockReset();
MockedClass.mockReset();
});- Type Safety
// Use type parameters for better type inference
const fn = mocker.fn<(x: number) => string>();
const obj = mocker.object<UserService>(service);- Avoid Over-mocking
// Bad: Mocking everything
const mockedService = mocker.object(service);
// Good: Mock only what you need
const mockedService = mocker.object(service, {
keepOriginal: ['helper1', 'helper2'],
});- Use Call Through When Needed
// Only mock what needs to be mocked
const mockedService = mocker.object(service, {
callThrough: true,
keepOriginal: ['util1', 'util2'],
});Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
License
This project is licensed under the Apache-2.0 License - see the LICENSE file for details.