@corez/mock
A powerful and flexible TypeScript mocking library for testing.

Table of Contents
Core Concepts
What is Mocking?
Mocking is a technique used in unit testing to isolate the code being tested by replacing dependencies with controlled
test doubles. This library provides three main types of test doubles:
-
Mocks: Complete replacements for dependencies
- Full control over behavior
- Verification capabilities
- Type-safe implementations
-
Spies: Wrappers around existing functions
- Track method calls
- Preserve original behavior
- Add verification capabilities
-
Stubs: Simple implementations
- Return predefined values
- No verification needed
- Minimal implementation
Type Safety
The library is built with TypeScript first in mind:
interface UserService {
getUser(id: number): Promise<User>;
updateUser(id: number, data: Partial<User>): Promise<void>;
}
const mockService = mock<UserService>();
mockService.getUser.mockResolvedValue({id: 1, name: 'User'});
mockService.getUser.mockImplementation((id: string) => Promise.resolve({id: 1}));
mockService.getUser.mockResolvedValue({id: 1});
Key type safety features:
- Full interface compliance
- Parameter type checking
- Return type validation
- Generic type support
- Strict null checks
- Method signature matching
Framework Integration
Works seamlessly with popular testing frameworks:
describe('UserService', () => {
let mockService: jest.Mocked<UserService>;
beforeEach(() => {
mockService = mock<UserService>();
});
it('should mock async methods', async () => {
mockService.getUser.mockResolvedValue({id: 1, name: 'Test'});
const result = await mockService.getUser(1);
expect(result.name).toBe('Test');
});
});
describe('UserService', () => {
let mockService: jasmine.SpyObj<UserService>;
beforeEach(() => {
mockService = mock<UserService>();
});
it('should track calls', () => {
mockService.getUser(1);
expect(mockService.getUser).toHaveBeenCalledWith(1);
});
});
Installation
npm install @corez/mock --save-dev
yarn add -D @corez/mock
pnpm add -D @corez/mock
API Reference
Core Functions
mock()
The primary function that intelligently determines how to create mocks based on the input type:
function mock<T>(): T;
function mock<T>(target: Class<T>): T;
function mock<T>(target: T): T;
function mock<T>(target?: T, options?: MockOptions<T>): T;
interface MockOptions<T> {
selective?: boolean;
preserveThis?: boolean;
handleCircular?: boolean;
implementation?: DeepPartial<T>;
debug?: boolean;
trackCalls?: boolean;
preservePrototype?: boolean;
}
interface UserService {
getUser(id: number): Promise<User>;
updateUser(user: User): Promise<void>;
}
const mockService = mock<UserService>();
mockService.getUser.mockResolvedValue({id: 1, name: 'Mock'});
mockService.updateUser.mockResolvedValue();
class DataService {
private data = new Map<string, any>();
async getData(id: string) {
return this.data.get(id);
}
}
const mockDataService = mock(DataService);
mockDataService.getData.mockResolvedValue({id: '1', data: 'mock'});
const realService = {
calculate: (x: number, y: number) => x + y,
config: {timeout: 1000},
};
const mockService = mock(realService);
mockService.calculate.mockReturnValue(42);
mockService.config.timeout = 2000;
const mockWithOptions = mock<UserService>({
selective: true,
preserveThis: true,
implementation: {
getUser: async id => ({id, name: 'Mock'}),
},
});
Key features:
- Intelligent type inference
- Full TypeScript support
- Automatic spy creation
- Property tracking
- Method call monitoring
- Framework compatibility (Jest/Jasmine)
- Chainable API support
mock.object()
Creates a fully mocked object where all properties and methods are spies:
function object<T extends object>(): T;
interface Service {
getData(): Promise<string>;
processValue(value: string): string;
config: {
timeout: number;
retries: number;
};
}
const mockService = mock.object<Service>();
mockService.getData.mockResolvedValue('test data');
mockService.processValue.mockImplementation(value => `processed-${value}`);
mockService.config = {
timeout: 1000,
retries: 3,
};
await mockService.getData();
mockService.processValue('input');
expect(mockService.getData).toHaveBeenCalled();
expect(mockService.processValue).toHaveBeenCalledWith('input');
Key features:
- Creates spies for all methods automatically
- Supports property tracking
- Handles nested objects
- Preserves type information
- Supports getters and setters
- Handles circular references
- Maintains property descriptors
mock.of()
Creates a mock from stubs, supporting both object and array types:
function of<T extends Array<any>>(stubs?: Array<DeepPartial<T[number]>>): MockOf<T>;
function of<T extends ReadonlyArray<any>>(stubs?: ReadonlyArray<DeepPartial<T[number]>>): MockOf<T>;
function of<T extends object>(stubs?: DeepPartial<T>): MockOf<T>;
interface User {
id: number;
name: string;
getData(): Promise<string>;
getDetails(): {age: number; email: string};
}
const mockUser = mock.of<User>({
id: 1,
name: 'John',
});
mockUser.getData.mockResolvedValue('test data');
mockUser.getDetails.mockReturnValue({age: 30, email: 'john@example.com'});
const mockWithMethod = mock.of<User>({
id: 1,
getData: async () => 'real data',
getDetails: () => ({age: 25, email: 'test@example.com'}),
});
expect(mockWithMethod.getData).toHaveBeenCalled();
interface Task {
id: number;
title: string;
complete(): Promise<void>;
}
const mockTasks = mock.of<Task[]>([
{id: 1, title: 'Task 1'},
{id: 2, title: 'Task 2'},
]);
mockTasks[0].complete.mockResolvedValue();
mockTasks[1].complete.mockRejectedValue(new Error('Failed'));
interface ComplexObject {
data: {
id: number;
nested: {
value: string;
};
};
getNestedValue(): string;
}
const mockComplex = mock.of<ComplexObject>({
data: {
id: 1,
nested: {
value: 'test',
},
},
getNestedValue: function () {
return this.data.nested.value;
},
});
Key features:
- Preserves provided values
- Automatically mocks undefined methods
- Supports array mocking
- Handles nested objects
- Maintains type safety
- Monitors method calls
- Supports method chaining
mock.cls()
Creates mock classes with full type safety and spy capabilities:
function cls<T extends new (...args: any[]) => any>(classConstructor: T, options?: ClsMockOptions<T>): ClsMock<T>;
interface ClsMockOptions<T extends new (...args: any[]) => any> {
selective?: boolean;
implementation?: DeepPartial<InstanceType<T>>;
}
class UserService {
private value: string;
constructor(initialValue: string = '') {
this.value = initialValue;
}
getValue(): string {
return this.value;
}
setValue(newValue: string): void {
this.value = newValue;
}
static staticMethod(): string {
return 'static';
}
}
const MockService = mock.cls(UserService);
const instance = new MockService('test');
expect(instance.getValue()).toBe('test');
expect(instance.getValue).toHaveBeenCalled();
expect(MockService.staticMethod()).toBe('static');
expect(MockService.staticMethod).toHaveBeenCalled();
const SelectiveMock = mock.cls(UserService, {
selective: true,
implementation: {
getValue: () => 'mocked',
},
});
const selectiveInstance = new SelectiveMock();
expect(selectiveInstance.getValue()).toBe('mocked');
expect(selectiveInstance.getValue).toHaveBeenCalled();
expect(SelectiveMock.staticMethod()).toBe('static');
class BaseClass {
static baseStatic() {
return 'base';
}
baseMethod() {
return 'base method';
}
}
class DerivedClass extends BaseClass {
static derivedStatic() {
return 'derived';
}
derivedMethod() {
return 'derived method';
}
}
const MockDerived = mock.cls(DerivedClass);
const derivedInstance = new MockDerived();
expect(MockDerived.baseStatic()).toBe('base');
expect(MockDerived.derivedStatic()).toBe('derived');
expect(derivedInstance.baseMethod()).toBe('base method');
expect(derivedInstance.derivedMethod()).toBe('derived method');
Key features:
- Mocks both instance and static methods
- Preserves original behavior by default
- Supports selective mocking
- Handles inheritance chain
- Maintains prototype chain
- Tracks all method calls
- Supports constructor arguments
- Type-safe implementation
mock.fn()
Creates mock functions with full spy capabilities:
function fn<T extends Fn = Fn>(): MockFunction<T>;
interface MockFunction<T extends Fn = Fn> extends Function {
(...args: Parameters<T>): ReturnType<T>;
mockReturnValue(value: ReturnType<T>): this;
mockResolvedValue<U>(value: U): MockFunction<AsyncFn & {(...args: Parameters<T>): Promise<U>}>;
mockImplementation(fn: T): this;
mockReset(): void;
mockClear(): void;
calls: {
all(): Array<{args: Parameters<T>}>;
count(): number;
};
toHaveBeenCalled(): boolean;
toHaveBeenCalledTimes(n: number): boolean;
toHaveBeenCalledWith(...args: Parameters<T>): boolean;
}
const mockFn = mock.fn();
mockFn.mockReturnValue('result');
mockFn('arg1');
expect(mockFn).toHaveBeenCalledWith('arg1');
expect(mockFn.calls.count()).toBe(1);
interface User {
id: number;
name: string;
}
const getUser = mock.fn<(id: number) => Promise<User>>();
getUser.mockResolvedValue({id: 1, name: 'Test User'});
await getUser(1);
expect(getUser).toHaveBeenCalledWith(1);
const calculate = mock.fn((x: number, y: number) => x * y);
expect(calculate(2, 3)).toBe(6);
expect(calculate).toHaveBeenCalledWith(2, 3);
const multiValue = mock.fn();
multiValue.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(3);
expect(multiValue()).toBe(1);
expect(multiValue()).toBe(2);
expect(multiValue()).toBe(3);
const fetchData = mock.fn<(query: string) => Promise<any>>();
fetchData.mockResolvedValueOnce({data: 'test'}).mockRejectedValueOnce(new Error('Network error'));
await fetchData('query1');
await expect(fetchData('query2')).rejects.toThrow('Network error');
const trackedFn = mock.fn();
trackedFn('a', 1);
trackedFn('b', 2);
expect(trackedFn.calls.count()).toBe(2);
expect(trackedFn.calls.all()).toEqual([{args: ['a', 1]}, {args: ['b', 2]}]);
Key features:
- Type-safe mock functions
- Flexible return values
- Async support
- Call tracking
- Chainable API
- Multiple return values
- Error simulation
- Framework compatibility
mock.partial()
Creates partial mocks from existing objects or class instances:
function partial<T extends object>(base: T, stubs?: DeepPartial<T>, options?: PartialOptions<T>): T | PartialBuilder<T>;
interface PartialOptions<T> {
selective?: boolean;
preserveThis?: boolean;
handleCircular?: boolean;
}
class UserService {
private value: string = 'original';
getValue(): string {
return this.value;
}
setValue(newValue: string): void {
this.value = newValue;
}
process(data: string): string {
return data.toUpperCase();
}
}
const service = new UserService();
const mockService = mock.partial(service, {
getValue: () => 'mocked',
});
expect(mockService.getValue()).toBe('mocked');
expect(mockService.getValue).toHaveBeenCalled();
expect(mockService.process('test')).toBe('TEST');
class DataService {
private data = new Map<string, any>();
constructor(initialData: Record<string, any> = {}) {
Object.entries(initialData).forEach(([key, value]) => {
this.data.set(key, value);
});
}
getData(key: string): any {
return this.data.get(key);
}
setData(key: string, value: any): void {
this.data.set(key, value);
}
}
const dataService = new DataService({key: 'value'});
const mockDataService = mock.partial(
dataService,
{
getData: function (this: DataService, key: string) {
return this.data.get(key)?.toUpperCase();
},
},
{
preserveThis: true,
},
);
mockDataService.setData('key', 'test');
expect(mockDataService.getData('key')).toBe('TEST');
expect(mockDataService.getData).toHaveBeenCalledWith('key');
interface Node {
value: string;
next?: Node;
}
const node: Node = {
value: 'root',
};
node.next = node;
const mockNode = mock.partial(
node,
{
value: 'mocked',
},
{
handleCircular: true,
},
);
expect(mockNode.value).toBe('mocked');
expect(mockNode.next?.value).toBe('mocked');
class Service {
getValue() {
return 'original';
}
transform(value: string) {
return value.toUpperCase();
}
}
const chainableMock = mock
.partial(new Service())
.spy('getValue')
.preserve('transform')
.with({
getValue: () => 'mocked',
});
expect(chainableMock.getValue()).toBe('mocked');
expect(chainableMock.getValue).toHaveBeenCalled();
expect(chainableMock.transform('test')).toBe('TEST');
Key features:
- Selective method mocking
- Original behavior preservation
- Context preservation
- Circular reference handling
- Property tracking
- Method call monitoring
- Chainable API support
- Type-safe implementation
mock.cast()
Casts an existing mock to a different type while preserving its behavior:
function cast<T, U>(mock: T): U;
interface BaseService {
getData(): string;
}
interface ExtendedService extends BaseService {
getExtendedData(): number;
}
const baseMock = mock.object<BaseService>();
const extendedMock = mock.cast<BaseService, ExtendedService>(baseMock);
baseMock.getData.mockReturnValue('data');
expect(extendedMock.getData()).toBe('data');
extendedMock.getExtendedData.mockReturnValue(42);
expect(extendedMock.getExtendedData()).toBe(42);
interface ComplexType {
nested: {
deep: {
value: number;
};
};
method(): string;
}
const partial = {
nested: {
deep: {
value: 42,
},
},
method: () => 'test',
};
const typed = mock.cast<ComplexType>(partial);
expect(typed.nested.deep.value).toBe(42);
expect(typed.method()).toBe('test');
interface Item {
id: number;
name: string;
}
const items = [
{id: 1, name: 'Item 1'},
{id: 2, name: 'Item 2'},
];
const typedItems = mock.cast<Item[]>(items);
expect(typedItems[0].id).toBe(1);
expect(typedItems[1].name).toBe('Item 2');
Key features:
- Type-safe casting
- Behavior preservation
- Property access
- Method call tracking
- Nested object support
- Array support
- Framework compatibility
mock.replace()
Temporarily replaces a method or property on an object:
function replace<T extends object, K extends keyof T>(obj: T, key: K, impl: T[K] & Function): void;
class UserService {
async getUser(id: number) {
return {id, name: 'Real User'};
}
getValue() {
return 'original';
}
}
const service = new UserService();
mock.replace(service, 'getUser', async id => {
return {id, name: 'Mock User'};
});
const user = await service.getUser(1);
expect(user.name).toBe('Mock User');
expect(service.getUser).toHaveBeenCalledWith(1);
const original = function () {
return 'original';
};
(original as any).customProp = 'test';
const obj = {method: original};
mock.replace(obj, 'method', () => 'mocked');
expect((obj.method as any).customProp).toBe('test');
expect(obj.method()).toBe('mocked');
class Service {
method1() {
return 'original1';
}
method2() {
return 'original2';
}
}
const svc = new Service();
mock.replace(svc, 'method1', () => 'mocked1');
mock.replace(svc, 'method2', () => 'mocked2');
expect(svc.method1()).toBe('mocked1');
expect(svc.method2()).toBe('mocked2');
expect(svc.method1).toHaveBeenCalled();
expect(svc.method2).toHaveBeenCalled();
mock.restore(svc, 'method1');
expect(svc.method1()).toBe('original1');
mock.restore(svc);
expect(svc.method2()).toBe('original2');
Key features:
- Temporary method replacement
- Original property preservation
- Method call tracking
- Multiple replacements
- Selective restoration
- Type safety
- Framework compatibility
mock.restore()
Restores replaced methods to their original implementations:
function restore<T extends object>(obj: T): boolean;
function restore<T extends object, K extends keyof T>(obj: T, key: K): boolean;
class UserService {
async getUser(id: number) {
return {id, name: 'Real User'};
}
}
const service = new UserService();
mock.replace(service, 'getUser', async id => ({id, name: 'Mock User'}));
const mockResult = await service.getUser(1);
expect(mockResult.name).toBe('Mock User');
const restored = mock.restore(service, 'getUser');
expect(restored).toBe(true);
const realResult = await service.getUser(1);
expect(realResult.name).toBe('Real User');
class Service {
method1() {
return 'original1';
}
method2() {
return 'original2';
}
}
const svc = new Service();
mock.replace(svc, 'method1', () => 'mocked1');
mock.replace(svc, 'method2', () => 'mocked2');
const replaced = mock.getReplacedMethods(svc);
expect(replaced).toEqual(['method1', 'method2']);
const batchRestored = mock.restore(svc);
expect(batchRestored).toBe(true);
class VerifyService {
getValue() {
return 'original';
}
}
const verifySvc = new VerifyService();
mock.replace(verifySvc, 'getValue', () => 'mocked');
expect(mock.isReplaced(verifySvc, 'getValue')).toBe(true);
mock.restore(verifySvc, 'getValue');
expect(mock.verifyRestored(verifySvc, 'getValue')).toBe(true);
class PropService {
method() {
return 'original';
}
}
const propSvc = new PropService();
const original = propSvc.method;
(original as any).customProp = 'test';
mock.replace(propSvc, 'method', () => 'mocked');
expect((propSvc.method as any).customProp).toBe('test');
mock.restore(propSvc, 'method');
expect((propSvc.method as any).customProp).toBe('test');
expect(propSvc.method()).toBe('original');
class InvalidService {
method() {}
}
const invalidSvc = new InvalidService();
const notRestored = mock.restore(invalidSvc, 'method');
expect(notRestored).toBe(false);
const invalidRestored = mock.restore(invalidSvc, 'nonexistent' as any);
expect(invalidRestored).toBe(false);
Key features:
- Batch restoration support
- Restoration status tracking
- Property preservation
- Restoration verification
- Method replacement checking
- Graceful error handling
- Type-safe implementation
- Framework compatibility
Configuration Methods
The library provides several methods to configure mocking behavior:
interface Config {
debug: boolean;
trackCalls: boolean;
allowUndefined: boolean;
strict: boolean;
preservePrototype: boolean;
handleCircular: boolean;
preserveThis: boolean;
selective: boolean;
}
const config = mock.getConfig();
mock.setConfig({
debug: true,
trackCalls: true,
allowUndefined: false,
strict: true,
preservePrototype: true,
handleCircular: true,
preserveThis: true,
selective: false,
});
mock.resetConfig();
mock.withConfig(
{
debug: true,
trackCalls: true,
},
() => {
const mockObj = mock.object<Service>();
mockObj.method();
},
);
await mock.withConfig(
{
preserveThis: true,
handleCircular: true,
},
async () => {
const mockService = mock.partial(realService);
await mockService.asyncMethod();
},
);
mock.withConfig({debug: true}, () => {
mock.withConfig({strict: true}, () => {
});
});
interface ConfigStats {
configChanges: number;
activeScopes: number;
defaultResets: number;
}
const stats = mock.getConfigStats();
Configuration options:
debug
: Enable debug logging (default: false in production)trackCalls
: Track method calls (default: true)allowUndefined
: Allow undefined properties (default: true)strict
: Throw on undefined properties (default: false)preservePrototype
: Preserve prototype chain (default: true)handleCircular
: Handle circular references (default: false)preserveThis
: Preserve original context (default: false)selective
: Only mock specified methods (default: false)
Best Practices
Testing Patterns
-
Clean State
- Reset mocks between tests
- Use beforeEach/afterEach hooks
- Avoid state leakage
-
Type Safety
- Always provide proper types
- Use interface mocking when possible
- Leverage TypeScript's type inference
-
Selective Mocking
- Mock only what you need
- Preserve original behavior when possible
- Use partial mocks for large classes
Common Pitfalls
-
Context Loss
- Use preserveThis option
- Be careful with arrow functions
- Maintain proper binding
-
Memory Leaks
- Reset mocks after use
- Clean up spies
- Handle circular references
-
Circular References
- Enable handleCircular option for objects with self-references
- Use mock.partial with handleCircular for complex objects
- Consider restructuring deeply nested objects
- Monitor memory usage in tests with circular structures
-
Type Safety
- Enable TypeScript strict mode (strict: true in tsconfig.json)
- Use explicit type parameters with mock functions
- Avoid type assertions unless necessary
- Leverage interface mocking for better type inference
Troubleshooting
Common Issues
- Type Inference Issues
Problem:
const mock = mock<Service>();
Solution:
interface Service {
method(): void;
}
const mockService = mock<Service>();
- This Context Lost
Problem:
class Service {
private value = 'test';
method() {
return this.value;
}
}
const mockService = mock.partial(new Service());
mockService.method();
Solution:
const mockService = mock.partial(
new Service(),
{},
{
preserveThis: true,
},
);
- Circular References
Problem:
interface Node {
next: Node;
}
const node = {next: null};
node.next = node;
Solution:
const mockNode = mock.partial(
node,
{},
{
handleCircular: true,
},
);
Error Messages
Common error messages and their solutions:
-
"Cannot spy on property which has no getter"
- Ensure the property exists on the target object
- Check if the property is accessible
- Use proper access modifiers
-
"Cannot mock non-function value"
- Verify the target is actually a function
- Check if the property is a getter/setter
- Ensure proper type definitions
-
"Maximum call stack size exceeded"
- Check for circular references
- Enable handleCircular option
- Review recursive mock implementations
- Consider using mock.partial with handleCircular option
-
"Property has a circular reference"
- Enable handleCircular in mock options
- Use mock.partial with handleCircular: true
- Consider restructuring the object graph
- Use WeakMap for circular reference tracking
-
"Accessing undefined property"
- Check if strict mode is enabled
- Verify property exists on mock object
- Consider using allowUndefined option
- Add explicit property definitions
-
"Invalid spy implementation"
- Ensure mock implementation matches original signature
- Check for proper this context binding
- Verify async/sync function compatibility
- Review method parameter types
Development
pnpm install
pnpm test
pnpm build
pnpm lint
pnpm format
pnpm test:coverage
For development, make sure to:
- Write tests for new features
- Update documentation for API changes
- Follow the TypeScript coding style
- Run the full test suite before submitting PR
TypeScript Configuration
The library is built with strict TypeScript settings:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
These strict settings ensure:
- No implicit any types
- Null and undefined checks
- Strict function type checking
- Proper this context handling
- Strict property initialization
- Index signature checking
- Exhaustive switch/case handling
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request at https://github.com/corezlab/mock
Please ensure your PR:
- Includes tests for new features
- Updates documentation as needed
- Follows the existing code style
- Includes a clear description of changes
Development Guidelines
-
Code Style
- Follow TypeScript best practices
- Use ESLint and Prettier
- Write clear comments
- Use meaningful variable names
-
Testing
- Write unit tests for new features
- Maintain test coverage
- Test edge cases
- Use meaningful test descriptions
-
Documentation
- Update README.md as needed
- Document new features
- Include examples
- Keep API documentation up to date
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.