@corez/mock
A powerful, flexible, and type-safe mocking library for TypeScript testing.

Features
- 🎯 Type Safety - Full TypeScript support with precise type inference
- 🔄 Deep Mocking - Automatic mocking of nested objects and methods
- 🕵️ Spy Tracking - Comprehensive call tracking and verification
- 🎭 Multiple Mocking Styles - Support for functions, objects, and classes
- 🔗 Inheritance Support - Proper handling of class inheritance and prototype chains
- 🎮 Intuitive API - Clean and chainable API design
- 🛡 Debug Support - Detailed logging for troubleshooting
- 🔄 In-Place Mocking - Option to modify original classes with restore capability
Installation
npm install @corez/mock --save-dev
yarn add -D @corez/mock
pnpm add -D @corez/mock
Quick Start
import {mock} from '@corez/mock';
const greet = mock.fn<(name: string) => string>();
greet.mockImplementation(name => `Hello, ${name}!`);
interface User {
id: number;
name: string;
}
interface UserService {
getUser(id: number): Promise<User>;
updateUser(user: User): Promise<void>;
}
const userService = mock.obj<UserService>(
{
getUser: async id => ({id, name: 'John'}),
updateUser: async user => {},
},
{
overrides: {
getUser: async id => ({id, name: 'Mock User'}),
},
},
);
class Database {
async connect() {
}
async query(sql: string) {
}
}
const MockDatabase = mock.cls(Database, {
mockStatic: true,
preserveConstructor: true,
});
const db = new MockDatabase();
db.query.mockResolvedValue({rows: []});
Core Concepts
Mock Functions
Create standalone mock functions with full tracking capabilities:
const mockFn = mock.fn<(x: number) => number>();
mockFn.mockImplementation(x => x * 2);
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue('result');
mockFn.mockRejectedValue(new Error('failed'));
expect(mockFn.calls.count()).toBe(1);
expect(mockFn.calls.all()[0].args).toEqual([1]);
Mock Objects
Create mock objects with automatic method tracking:
interface UserService {
getUser(id: number): Promise<User>;
updateUser(user: User): Promise<void>;
}
const userService = mock.obj<UserService>({
getUser: async id => ({id, name: 'John'}),
updateUser: async user => {},
});
userService.getUser(1);
expect((userService.getUser as any).mock.calls.length).toBe(1);
(userService.getUser as any).mockResolvedValue({id: 1, name: 'Mock'});
const getUserMock = userService.getUser as MockFunction;
console.log(getUserMock.calls.count());
console.log(getUserMock.calls.all());
Advanced Object Mocking Features
- Prototype Chain Mocking
const service = mock.obj(originalService, {
prototypeChain: true,
});
- Arrow Functions vs Regular Methods The library automatically detects and handles arrow functions differently to
preserve correct
this
binding:
class Service {
regularMethod() {
return this;
}
arrowMethod = () => this;
}
const mockedService = mock.obj(new Service());
- In-Place Mocking
const service = mock.obj(originalService, {
inPlace: true,
});
Mock Classes
Create mock classes with automatic method tracking:
class Database {
async connect() {
}
async query(sql: string) {
}
}
const MockDatabase = mock.cls(Database, {
static: true,
preserveConstructor: true,
});
console.log(MockDatabase.__is_mocked__);
console.log(MockDatabase[IS_MOCKED]);
const db = new MockDatabase();
const queryMock = db.query as MockFunction;
console.log(queryMock.calls.count());
Factory Mocking
Create factory-based mock classes that automatically mock instances on construction:
class Database {
async connect() {
}
async query(sql: string) {
}
}
const MockDatabase = mock.factory(Database, {
prototypeChain: true,
});
const db = new MockDatabase();
expect(db.query).toHaveProperty('mock');
db.query.mockResolvedValue({rows: []});
db.query('SELECT * FROM users');
expect(db.query).toHaveBeenCalled();
mock.factory(Database, {
inPlace: true,
prototypeChain: true,
});
const db2 = new Database();
db2.query.mockResolvedValue({rows: []});
Key features of factory mocking:
- Automatic instance mocking on construction
- Proper inheritance handling using
Reflect.construct
- Support for prototype chain mocking
- In-place modification option
- Automatic spy tracking for all methods
- Preserves constructor behavior
Type Definitions
The library provides comprehensive type definitions for mocked classes:
type ClsMock<T extends Constructor<any>> = T & {
__is_mocked__: boolean;
[IS_MOCKED]: true;
new (...args: ConstructorParameters<T>): InstanceType<T>;
};
const mockDb: ClsMock<typeof Database> = mock.cls(Database);
if (mockDb.__is_mocked__) {
console.log('Class is mocked');
}
API Reference
Core APIs
mock.fn<T extends Fn = Fn>(): MockFunction<T>
Creates a mock function with tracking capabilities:
const mockFn = mock.fn<(x: number) => number>();
mockFn.mockImplementation(x => x * 2);
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue('result');
mockFn.mockRejectedValue(new Error('failed'));
expect(mockFn.calls.count()).toBe(1);
expect(mockFn.calls.all()[0].args).toEqual([1]);
mock.obj<T extends object>(target: T | undefined, options?: ObjectMockOptions<T>): MockObject<T>
Creates a mock object with automatic method tracking. Available options:
interface ObjectMockOptions<T> {
inPlace?: boolean;
prototypeChain?: boolean;
overrides?: Partial<T>;
}
Key features:
- Deep cloning support using
rfdc
- Automatic handling of arrow functions vs regular methods
- Prototype chain preservation and mocking
- Property descriptor preservation
- Spy tracking for all methods
mock.cls<T extends Constructor<any>>(target: T, options?: ClassMockOptions<T>): ClsMock<T>
Creates a mock class with automatic method tracking and type safety:
const MockDatabase = mock.cls(Database, {
inPlace: false,
preservePrototype: true,
preserveConstructor: true,
overrides: {
query: async () => [{id: 1}],
},
debug: false,
});
if (MockDatabase.__is_mocked__) {
console.log('Class is properly mocked');
}
const instance = new MockDatabase();
const queryMock = instance.query as MockFunction;
console.log(queryMock.calls.count());
The ClassMockOptions
interface provides fine-grained control:
interface ClassMockOptions<T> {
inPlace?: boolean;
preservePrototype?: boolean;
preserveConstructor?: boolean;
overrides?: Partial<T>;
debug?: boolean;
}
mock.factory<T extends Constructor<any>>(target: T, options?: ObjectMockOptions): T
Creates a factory-based mock class that automatically mocks instances on construction:
const MockDatabase = mock.factory(Database, {
prototypeChain: true,
inPlace: false,
overrides: {
query: async () => [{id: 1}],
},
debug: false,
});
const db = new MockDatabase();
db.query.mockResolvedValue({rows: []});
db.query('SELECT * FROM users');
expect(db.query).toHaveBeenCalled();
The factory function provides these key features:
- Automatic instance mocking on construction
- Proper inheritance handling using
Reflect.construct
- Support for prototype chain mocking
- In-place modification option
- Automatic spy tracking for all methods
- Preserves constructor behavior
Available options:
interface FactoryOptions extends ObjectMockOptions {
prototypeChain?: boolean;
inPlace?: boolean;
overrides?: Partial<T>;
debug?: boolean;
}
mock.compose<T extends Fn>(): MockFunction<T>;
mock.compose<T extends new (...args: any[]) => any>(target: T, options?: {overrides?: DeepPartial<InstanceType<T>>} & Partial<Config>): ClsMock<T>;
mock.compose<T extends object>(target: T, options?: {overrides?: DeepPartial<T>; replace?: {[K in keyof T]?: T[K] extends Fn ? Fn : never}} & Partial<Config>): T;
mock.compose<T extends object>(partialImpl: DeepPartial<T>, options?: Partial<Config>): T;
Creates a mock from a class constructor, object, or function:
const mockFn = mock.compose<(x: number) => string>();
mockFn.mockImplementation(x => x.toString());
class Database {
async query(sql: string) {
}
}
const MockDatabase = mock.compose(Database, {
overrides: {
query: async () => [{id: 1}],
},
});
interface Api {
fetch(url: string): Promise<any>;
}
const api = mock.compose<Api>(
{
fetch: async url => ({data: []}),
},
{
overrides: {
fetch: async url => ({data: [{id: 1}]}),
},
},
);
interface ComplexApi {
getUsers(): Promise<User[]>;
getUser(id: number): Promise<User>;
createUser(user: User): Promise<void>;
}
const partialApi = mock.compose<ComplexApi>({
getUsers: async () => [{id: 1, name: 'John'}],
getUser: async id => ({id, name: 'John'}),
});
mock.cast<T extends object>(partial: DeepPartial<T>, options?: Partial<Config>): T
Casts a partial implementation to a complete mock:
interface CompleteApi {
getUsers(): Promise<User[]>;
getUser(id: number): Promise<User>;
createUser(user: User): Promise<void>;
updateUser(user: User): Promise<void>;
deleteUser(id: number): Promise<void>;
}
const api = mock.cast<CompleteApi>({
getUsers: async () => [{id: 1, name: 'John'}],
getUser: async id => ({id, name: 'John'}),
});
await api.createUser({id: 1, name: 'Test'});
await api.updateUser({id: 1, name: 'Test'});
await api.deleteUser(1);
const createUserMock = api.createUser as MockFunction;
expect(createUserMock.calls.count()).toBe(1);
mock.replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>): void
Replaces methods while preserving original implementation:
class Service {
async getData() {
}
async processData(data: any) {
}
}
const service = new Service();
mock.replace(service, 'getData', async () => ['mocked']);
expect(await service.getData()).toEqual(['mocked']);
expect(service.processData).toBeDefined();
const getDataMock = service.getData as unknown as MockFunction;
expect(getDataMock.calls.count()).toBe(1);
mock.restore(service);
Mock Control
All mocks provide these control methods:
mock.mockClear();
mock.mockReset();
mock.mockRestore();
mock.calls.all();
mock.calls.count();
Advanced Usage
Async Mocking
Handle async operations:
const api = mock.obj<Api>(
{},
{
overrides: {
fetch: async url => {
if (url === '/users') {
return [{id: 1}];
}
throw new Error('Not found');
},
},
},
);
Partial Mocking
Selectively mock methods:
class UserService {
async getUser(id: number) {
}
async validate(user: User) {
}
}
const service = new UserService();
mock.replace(service, 'getUser', async id => ({
id,
name: 'Mock User',
}));
const partialService = mock.obj<UserService>(
{},
{
overrides: {
getUser: async id => ({id, name: 'Mock User'}),
},
},
);
Best Practices
-
Reset Between Tests
beforeEach(() => {
mockFn.mockReset();
mockObj.mockReset();
});
-
Type Safety
interface Service {
method(): string;
}
const mock = mock.obj<Service>();
-
Error Handling
api.fetch.mockRejectedValue(new Error('Network error'));
await expect(api.fetch()).rejects.toThrow('Network error');
-
Verification
expect(mockFn.calls.count()).toBe(1);
expect(mockFn.calls.all()[0].args).toEqual(['expected arg']);
-
Using inPlace
const mockObj = mock.obj(original, {
inPlace: true,
overrides: {
method: () => 'mocked',
},
});
const MockClass = mock.cls(Original);
const ModifiedClass = mock.cls(Original, {
inPlace: true,
});
Troubleshooting
Common Issues
-
Mock Not Tracking Calls
- For functions: Enable
trackCalls
in FunctionMockOptions - For objects and classes: Method calls are tracked automatically
- Ensure you're accessing the mock properties correctly (e.g.,
mock.calls
)
-
Type Inference Issues
- Use explicit type parameters with interfaces
- Define complete interfaces for complex types
- Use
as MockFunction<T>
for better type support
-
Prototype Chain Issues
- For classes: Use
preservePrototype: true
(default) - For objects: Use
prototypeChain: true
to mock prototype methods (default) - For type casting: Use
keepPrototype: true
(default)
-
inPlace Issues
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
Apache-2.0 - see LICENSE for details.
Mock Options
Base Options
All mock types support these basic options:
debug
: Enables detailed logging of mock operations (default: false)asyncTimeout
: Maximum time to wait for async operations in ms (default: 5000)
Function Mock Options
Options for mocking functions:
trackCalls
: Records arguments, return values, and call count (default: false)autoSpy
: Automatically creates spies for all function properties (default: false)
Object Mock Options
Options for mocking objects:
inPlace
: Controls whether to modify the original object or create a new one (default: false)prototypeChain
: Controls whether to perform deep cloning and mocking of nested objects and prototype chain methods
(default: true)overrides
: Allows overriding specific properties or methods with custom implementations
Example of object mocking with inPlace:
const mockObj = mock.obj(original, {
overrides: {
method: () => 'mocked',
},
});
const modifiedObj = mock.obj(original, {
inPlace: true,
overrides: {
method: () => 'mocked',
},
});
const deepMockObj = mock.obj(original, {
prototypeChain: true,
});
Class Mock Options
Options for mocking classes:
inPlace
: Controls whether to modify the original class or create a new one (default: false)preservePrototype
: Controls whether to preserve the prototype chain (default: true)preserveConstructor
: Controls whether to preserve the original constructor behavior (default: true)mockStatic
: Controls whether to mock static class members (default: false)overrides
: Allows overriding specific properties or methods with custom implementations
Example of class mocking with inPlace:
const MockClass = mock.cls(Original, {
static: true,
overrides: {
method: () => 'mocked',
},
});
const ModifiedClass = mock.cls(Original, {
inPlace: true,
static: true,
overrides: {
method: () => 'mocked',
},
});
Cast Mock Options
Options for type casting and partial implementations:
keepPrototype
: Maintains the prototype chain when casting objects (default: true)
Usage Examples
Basic Function Mocking
import {mock} from '@corez/mock';
const mockFn = mock.fn<() => string>();
mockFn.mockReturnValue('hello');
expect(mockFn()).toBe('hello');
expect(mockFn.mock.calls.length).toBe(1);
Object Mocking
import {mock} from '@corez/mock';
interface User {
name: string;
getId(): number;
}
const mockUser = mock.obj<User>({
name: 'Test User',
getId: () => 1,
});
mockUser.getId.mockReturnValue(2);
Class Mocking
import {mock} from '@corez/mock';
class Database {
connect() {
}
query(sql: string) {
}
}
const MockDatabase = mock.cls(Database, {
mockStatic: true,
preserveConstructor: true,
});
const db = new MockDatabase();
db.query.mockResolvedValue({rows: []});
Type Casting
import {mock} from '@corez/mock';
interface ComplexType {
data: string;
process(): Promise<void>;
}
const partial = {data: 'test'};
const mockObj = mock.cast<ComplexType>(partial);
Mock Composition
Compose Function Options
The compose
function provides a unified interface for creating mocks of different types (classes, objects, and
functions). It uses a sophisticated type system to provide appropriate options based on the target type.
Options Structure
type ComposeOptions<T> = BaseMockOptions &
(T extends new (...args: any[]) => any
? ClassMockOptions<InstanceType<T>>
: ObjectMockOptions<T> & {
replace?: {
[K in keyof T]?: T[K] extends Fn ? Fn : never;
};
});
Design Decisions
-
Replace vs Override
replace
is specifically designed for object mocking, allowing direct method replacementoverride
is used in class mocking for overriding prototype methods- While they may seem similar, they serve different purposes and contexts
-
Type Safety
- The options type automatically adapts based on the target type
- Class mocks receive
ClassMockOptions
- Object mocks receive
ObjectMockOptions
with additional replace
capability - This ensures type safety while maintaining flexibility
-
Usage Examples
class Service {
getData(): string {
return 'data';
}
}
const mockService = compose(Service, {
override: {
getData: () => 'mocked',
},
});
const obj = {
method: () => 'original',
};
const mockObj = compose(obj, {
replace: {
method: () => 'replaced',
},
});
- Best Practices
- Use
override
for class method mocking - Use
replace
for direct object method replacement - Consider the scope and context when choosing between them
- Avoid mixing
override
and replace
in the same mock
Debugging Features
Debug Options
The library provides comprehensive debugging capabilities through the debug