@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());
Mock Classes
Create mock classes with automatic method tracking:
class Database {
async connect() {
}
async query(sql: string) {
}
}
const MockDatabase = mock.cls(Database, {
overrides: {
query: async () => [{id: 1}],
},
});
const db = new MockDatabase();
const queryMock = db.query as MockFunction;
console.log(queryMock.calls.count());
mock.cls(Database, {
mockInPlace: true,
overrides: {
query: async () => [{id: 1}],
},
});
const originalQuery = Database.prototype.query;
const originalConnect = Database.prototype.connect;
Database.prototype.query = originalQuery;
Database.prototype.connect = originalConnect;
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?: ObjMockOptions<T>): MockObject<T>
Creates a mock object 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 => {},
},
{
overrides: {
getUser: async id => ({id, name: 'Mock User'}),
},
},
);
const getUserMock = userService.getUser as MockFunction;
console.log(getUserMock.calls.count());
console.log(getUserMock.calls.all());
expect(getUserMock.calls.count()).toBe(1);
expect(getUserMock.calls.all()[0].args).toEqual([1]);
mock.cls<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T>
Creates a mock class with automatic method tracking:
class Database {
async connect() {
}
async query(sql: string) {
}
}
const MockDatabase = mock.cls(Database, {
overrides: {
query: async () => [{id: 1}],
},
});
const db = new MockDatabase();
const queryMock = db.query as MockFunction;
console.log(queryMock.calls.count());
mock.cls(Database, {
mockInPlace: true,
overrides: {
query: async () => [{id: 1}],
},
});
const originalQuery = Database.prototype.query;
const originalConnect = Database.prototype.connect;
Database.prototype.query = originalQuery;
Database.prototype.connect = originalConnect;
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']);
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
mockDeep: true
to mock prototype methods - For type casting: Use
keepPrototype: true
(default)
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:
mockInPlace
: Controls whether to modify the original object or create a new one (default: false)mockDeep
: Controls whether to perform deep cloning and mocking of nested objects and prototype chain methods
(default: false)overrides
: Allows overriding specific properties or methods with custom implementations
Class Mock Options
Options for mocking classes:
mockInPlace
: 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
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
option and name
property:
interface DebugOptions {
debug?: boolean;
name?: string;
level?: DebugLevel;
logPrefix?: string;
logToConsole?: boolean;
customLogger?: (message: string, level: DebugLevel) => void;
}
const userService = mock.compose<UserService>(
{},
{
debug: true,
name: 'UserService',
level: DebugLevel.DEBUG,
logPrefix: '[TEST]',
},
);
Debug Levels
The library supports different debug levels for fine-grained control:
ERROR
: Only critical errorsWARN
: Warnings and errorsINFO
: General information (default)DEBUG
: Detailed debugging informationTRACE
: Most verbose level
What Gets Logged
When debugging is enabled, the following information is logged:
-
Method Calls:
- Method name
- Arguments passed
- Return values
- Execution time (for async methods)
-
Mock Creation:
- Type of mock (function/object/class)
- Configuration options used
- Initial state
-
Errors and Warnings:
- Error messages
- Stack traces (in DEBUG level)
- Warning conditions
Custom Logging
You can provide your own logging implementation:
const customLogger = (message: string, level: DebugLevel) => {
myLoggingService.log({
message,
level,
timestamp: new Date(),
});
};
const mock = mock.compose<UserService>(
{},
{
debug: true,
name: 'UserService',
customLogger,
},
);
Best Practices for Debugging
- Use Meaningful Names:
const userServiceMock = mock.compose<UserService>({}, {name: 'UserService'});
const mock1 = mock.compose<UserService>({});
- Set Appropriate Debug Levels:
const devMock = mock.compose<UserService>(
{},
{
debug: true,
level: DebugLevel.DEBUG,
},
);
const prodMock = mock.compose<UserService>(
{},
{
debug: true,
level: DebugLevel.ERROR,
},
);
- Use Custom Logging for CI/CD:
const ciLogger = (message: string, level: DebugLevel) => {
if (process.env.CI) {
process.stdout.write(`::${level}::${message}\n`);
}
};
Logging and Debugging
Logging Configuration
The library provides comprehensive logging capabilities to help with debugging and troubleshooting. You can configure
logging at different levels:
import {mock, LogLevel} from '@corez/mock';
const mockFn = mock.fn({
debug: true,
});
const userService = mock.obj<UserService>({
logging: {
level: LogLevel.DEBUG,
prefix: '[UserService]',
consoleOutput: true,
formatter: (msg, level, ctx) => {
return `[${new Date().toISOString()}] ${level}: ${msg}`;
},
logger: (msg, level) => {
customLogger.log(msg, level);
},
},
});
Log Levels
The library supports the following log levels:
LogLevel.NONE
(0) - Disable all loggingLogLevel.ERROR
(1) - Only log errorsLogLevel.WARN
(2) - Log warnings and errorsLogLevel.INFO
(3) - Log general information (default)LogLevel.DEBUG
(4) - Log detailed debug informationLogLevel.TRACE
(5) - Log everything including internal details
Logging Options
Option | Type | Default | Description |
---|
debug | boolean | false | Quick way to enable detailed logging |
level | LogLevel | LogLevel.INFO | Controls log verbosity |
prefix | string | mock name | Custom prefix for log messages |
consoleOutput | boolean | true if debug=true | Whether to write logs to console |
formatter | Function | undefined | Custom log message formatter |
logger | Function | console.log | Custom logging implementation |
Debug Information
When logging is enabled, the library will output information about:
- Mock creation and configuration
- Function calls and arguments
- Return values and errors
- Mock state changes
- Spy tracking information
- Method overrides and restorations
Example debug output:
[UserService] DEBUG: Creating mock object
[UserService] INFO: Method 'getUser' called with args: [1]
[UserService] DEBUG: Return value: { id: 1, name: 'Mock User' }
[UserService] WARN: Method 'updateUser' not implemented