@corez/mock
Advanced tools
Comparing version 0.9.1 to 0.10.0
@@ -120,3 +120,3 @@ /** | ||
*/ | ||
inPlace?: boolean; | ||
inplace?: boolean; | ||
/** | ||
@@ -155,6 +155,16 @@ * Partial implementation to override specific properties or methods | ||
* When false, only mocks the top level object's own methods | ||
* Only applies when inPlace is false | ||
* Only applies when inplace is false | ||
* @default false | ||
*/ | ||
prototypeChain?: boolean; | ||
/** | ||
* Glob patterns for methods/properties to mock | ||
* Supports ! prefix for exclusion | ||
* Examples: | ||
* - ['get*'] - mock all methods starting with 'get' | ||
* - ['!private*'] - exclude all methods starting with 'private' | ||
* - ['handle{Get,Post}*'] - mock all methods starting with handleGet or handlePost | ||
* @default undefined - mocks everything except constructor | ||
*/ | ||
patterns?: string[]; | ||
} | ||
@@ -179,7 +189,24 @@ /** | ||
/** | ||
* Controls whether to mock static class members | ||
* Includes static methods, properties, and getters/setters | ||
* Glob patterns for methods/properties to mock | ||
* Supports ! prefix for exclusion | ||
* Examples: | ||
* - ['get*'] - mock all methods starting with 'get' | ||
* - ['!private*'] - exclude all methods starting with 'private' | ||
* - ['handle{Get,Post}*'] - mock all methods starting with handleGet or handlePost | ||
* @default undefined - mocks everything except constructor | ||
*/ | ||
patterns?: string[]; | ||
/** | ||
* Controls static member mocking | ||
* Can be boolean to mock all/none static members | ||
* Or array of glob patterns with ! prefix for exclusion | ||
* @default false | ||
*/ | ||
static?: boolean; | ||
static?: boolean | string[]; | ||
/** | ||
* Controls whether to mock inherited methods | ||
* When false, only mocks methods defined directly on the class | ||
* @default true | ||
*/ | ||
inheritance?: boolean; | ||
} | ||
@@ -279,6 +306,2 @@ /** | ||
/** | ||
* Creates a factory-based mock class that automatically mocks instances on construction | ||
*/ | ||
factory<T extends Constructor<any>>(target: T, options?: ObjectMockOptions): T; | ||
/** | ||
* Casts a partial implementation to a complete mock | ||
@@ -285,0 +308,0 @@ */ |
{ | ||
"name": "@corez/mock", | ||
"version": "0.9.1", | ||
"version": "0.10.0", | ||
"description": "A powerful and flexible TypeScript mocking library for testing", | ||
@@ -55,2 +55,3 @@ "keywords": [ | ||
"@types/jest": "^29.5.14", | ||
"@types/micromatch": "^4.0.9", | ||
"@types/node": "^22.10.5", | ||
@@ -68,2 +69,3 @@ "@typescript-eslint/eslint-plugin": "^8.19.1", | ||
"jest": "^29.7.0", | ||
"micromatch": "^4.0.8", | ||
"prettier": "^3.4.2", | ||
@@ -70,0 +72,0 @@ "release-it": "^18.1.1", |
784
README.md
@@ -10,13 +10,15 @@ # @corez/mock | ||
## 目录 | ||
- [Features](#features) | ||
- [Installation](#installation) | ||
- [Quick Start](#quick-start) | ||
- [Core Concepts](#core-concepts) | ||
- [API Reference](#api-reference) | ||
- [Core APIs](#core-apis) | ||
- [Advanced Usage](#advanced-usage) | ||
- [Mock Options](#mock-options) | ||
- [Best Practices](#best-practices) | ||
- [Debugging](#debugging) | ||
- [Troubleshooting](#troubleshooting) | ||
- [Contributing](#contributing) | ||
- [License](#license) | ||
- [Debugging Features](#debugging-features) | ||
@@ -57,7 +59,2 @@ ## Features | ||
// Mock an object | ||
interface User { | ||
id: number; | ||
name: string; | ||
} | ||
interface UserService { | ||
@@ -68,13 +65,6 @@ getUser(id: number): Promise<User>; | ||
const userService = mock.obj<UserService>( | ||
{ | ||
getUser: async id => ({id, name: 'John'}), | ||
updateUser: async user => {}, | ||
}, | ||
{ | ||
overrides: { | ||
getUser: async id => ({id, name: 'Mock User'}), | ||
}, | ||
}, | ||
); | ||
const userService = mock.obj<UserService>({ | ||
getUser: async id => ({id, name: 'John'}), | ||
updateUser: async user => {}, | ||
}); | ||
@@ -91,3 +81,2 @@ // Mock a class | ||
// Create a new mock class | ||
const MockDatabase = mock.cls(Database, { | ||
@@ -102,5 +91,5 @@ mockStatic: true, | ||
## Core Concepts | ||
## Core APIs | ||
### Mock Functions | ||
### mock.fn() - Function Mocking | ||
@@ -127,3 +116,3 @@ Create standalone mock functions with full tracking capabilities: | ||
### Mock Objects | ||
### mock.obj() - Object Mocking | ||
@@ -149,43 +138,6 @@ Create mock objects with automatic method tracking: | ||
(userService.getUser as any).mockResolvedValue({id: 1, name: 'Mock'}); | ||
// Access call information | ||
const getUserMock = userService.getUser as MockFunction; | ||
console.log(getUserMock.calls.count()); | ||
console.log(getUserMock.calls.all()); | ||
``` | ||
#### Advanced Object Mocking Features | ||
### mock.cls() - Class Mocking | ||
1. **Prototype Chain Mocking** | ||
```typescript | ||
const service = mock.obj(originalService, { | ||
prototypeChain: true, // Will mock methods from entire prototype chain | ||
}); | ||
``` | ||
2. **Arrow Functions vs Regular Methods** The library automatically detects and handles arrow functions differently to | ||
preserve correct `this` binding: | ||
```typescript | ||
class Service { | ||
regularMethod() { | ||
return this; | ||
} | ||
arrowMethod = () => this; | ||
} | ||
const mockedService = mock.obj(new Service()); | ||
// Both methods will maintain correct `this` binding | ||
``` | ||
3. **In-Place Mocking** | ||
```typescript | ||
const service = mock.obj(originalService, { | ||
inPlace: true, // Modifies the original object | ||
}); | ||
``` | ||
### Mock Classes | ||
Create mock classes with automatic method tracking: | ||
@@ -203,13 +155,13 @@ | ||
// Create a new mock class | ||
const MockDatabase = mock.cls(Database, { | ||
static: true, | ||
mockStatic: true, | ||
preserveConstructor: true, | ||
}); | ||
const db = new MockDatabase(); | ||
db.query.mockResolvedValue({rows: []}); | ||
// Check if class is mocked | ||
console.log(MockDatabase.__is_mocked__); // true | ||
console.log(MockDatabase[IS_MOCKED]); // true | ||
const db = new MockDatabase(); | ||
// Access call information | ||
@@ -220,17 +172,7 @@ const queryMock = db.query as MockFunction; | ||
### Factory Mocking | ||
### mock.factory() - Factory Mocking | ||
Create factory-based mock classes that automatically mock instances on construction: | ||
Create factory-based mock classes that automatically mock instances: | ||
```typescript | ||
class Database { | ||
async connect() { | ||
/* ... */ | ||
} | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
// Create a factory mock class | ||
const MockDatabase = mock.factory(Database, { | ||
@@ -242,3 +184,3 @@ prototypeChain: true, // Mock all prototype methods | ||
const db = new MockDatabase(); | ||
expect(db.query).toHaveProperty('mock'); // true | ||
expect(db.query).toHaveProperty('mock'); | ||
@@ -251,221 +193,9 @@ // Mock implementation | ||
expect(db.query).toHaveBeenCalled(); | ||
// In-place mocking | ||
mock.factory(Database, { | ||
inPlace: true, // Modify original class | ||
prototypeChain: true, | ||
}); | ||
// Now original class creates mocked instances | ||
const db2 = new Database(); | ||
db2.query.mockResolvedValue({rows: []}); | ||
``` | ||
Key features of factory mocking: | ||
### mock.compose() - Composition 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 | ||
Create mocks from classes, objects, or functions: | ||
### Type Definitions | ||
The library provides comprehensive type definitions for mocked classes: | ||
```typescript | ||
// Mock class type that includes mock markers | ||
type ClsMock<T extends Constructor<any>> = T & { | ||
__is_mocked__: boolean; | ||
[IS_MOCKED]: true; | ||
new (...args: ConstructorParameters<T>): InstanceType<T>; | ||
}; | ||
// Usage with type checking | ||
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: | ||
```typescript | ||
const mockFn = mock.fn<(x: number) => number>(); | ||
// Set implementation | ||
mockFn.mockImplementation(x => x * 2); | ||
// Set return value | ||
mockFn.mockReturnValue(42); | ||
// Handle async scenarios | ||
mockFn.mockResolvedValue('result'); | ||
mockFn.mockRejectedValue(new Error('failed')); | ||
// Verify calls | ||
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: | ||
```typescript | ||
interface ObjectMockOptions<T> { | ||
// If true, modifies the original object instead of creating a clone | ||
inPlace?: boolean; | ||
// If true, mocks methods from the entire prototype chain | ||
prototypeChain?: boolean; | ||
// Override specific properties or methods | ||
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: | ||
```typescript | ||
// Create a mock class with options | ||
const MockDatabase = mock.cls(Database, { | ||
// Modify original class instead of creating new one | ||
inPlace: false, | ||
// Preserve original method implementations | ||
preservePrototype: true, | ||
// Call original constructor | ||
preserveConstructor: true, | ||
// Override specific methods | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
// Enable debug logging | ||
debug: false, | ||
}); | ||
// Type checking and verification | ||
if (MockDatabase.__is_mocked__) { | ||
console.log('Class is properly mocked'); | ||
} | ||
// Access mock information | ||
const instance = new MockDatabase(); | ||
const queryMock = instance.query as MockFunction; | ||
console.log(queryMock.calls.count()); | ||
``` | ||
The `ClassMockOptions` interface provides fine-grained control: | ||
```typescript | ||
interface ClassMockOptions<T> { | ||
// Whether to modify the original class | ||
inPlace?: boolean; | ||
// Whether to preserve original method implementations | ||
preservePrototype?: boolean; | ||
// Whether to call original constructor | ||
preserveConstructor?: boolean; | ||
// Method overrides | ||
overrides?: Partial<T>; | ||
// Enable debug logging | ||
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: | ||
```typescript | ||
// Create a factory mock class | ||
const MockDatabase = mock.factory(Database, { | ||
// Mock all prototype chain methods | ||
prototypeChain: true, | ||
// Modify original class instead of creating new one | ||
inPlace: false, | ||
// Override specific methods | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
// Enable debug logging | ||
debug: false, | ||
}); | ||
// Each instance is automatically mocked | ||
const db = new MockDatabase(); | ||
// Mock implementation | ||
db.query.mockResolvedValue({rows: []}); | ||
// Track calls | ||
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: | ||
```typescript | ||
interface FactoryOptions extends ObjectMockOptions { | ||
// When true, mocks all prototype chain methods | ||
prototypeChain?: boolean; | ||
// When true, modifies original class | ||
inPlace?: boolean; | ||
// Override specific methods | ||
overrides?: Partial<T>; | ||
// Enable debug logging | ||
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: | ||
```typescript | ||
// Mock a function | ||
@@ -476,8 +206,2 @@ const mockFn = mock.compose<(x: number) => string>(); | ||
// Mock a class | ||
class Database { | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
const MockDatabase = mock.compose(Database, { | ||
@@ -490,6 +214,2 @@ overrides: { | ||
// Mock an object | ||
interface Api { | ||
fetch(url: string): Promise<any>; | ||
} | ||
const api = mock.compose<Api>( | ||
@@ -505,19 +225,7 @@ { | ||
); | ||
// Mock with partial implementation | ||
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` | ||
### mock.cast() - Type Casting | ||
Casts a partial implementation to a complete mock: | ||
Cast partial implementations to complete mocks: | ||
@@ -533,3 +241,3 @@ ```typescript | ||
// Only implement the methods we need | ||
// Only implement needed methods | ||
const api = mock.cast<CompleteApi>({ | ||
@@ -540,15 +248,9 @@ getUsers: async () => [{id: 1, name: 'John'}], | ||
// All other methods will be automatically mocked | ||
await api.createUser({id: 1, name: 'Test'}); // Works, returns undefined | ||
await api.updateUser({id: 1, name: 'Test'}); // Works, returns undefined | ||
await api.deleteUser(1); // Works, returns undefined | ||
// Access call information | ||
const createUserMock = api.createUser as MockFunction; | ||
expect(createUserMock.calls.count()).toBe(1); | ||
// All other methods automatically mocked | ||
await api.createUser({id: 1, name: 'Test'}); | ||
``` | ||
#### `mock.replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>): void` | ||
### mock.replace() - Method Replacement | ||
Replaces methods while preserving original implementation: | ||
Replace methods while preserving original implementation: | ||
@@ -560,3 +262,2 @@ ```typescript | ||
} | ||
async processData(data: any) { | ||
@@ -572,37 +273,63 @@ /* ... */ | ||
// Verify the method was replaced | ||
// Verify replacement | ||
expect(await service.getData()).toEqual(['mocked']); | ||
// Original processData method remains unchanged | ||
// Original processData remains unchanged | ||
expect(service.processData).toBeDefined(); | ||
// Access call information | ||
const getDataMock = service.getData as unknown as MockFunction; | ||
expect(getDataMock.calls.count()).toBe(1); | ||
// Restore original implementation | ||
// Restore original | ||
mock.restore(service); | ||
``` | ||
### Mock Control | ||
## Mock Options | ||
All mocks provide these control methods: | ||
### Base Options | ||
```typescript | ||
// Clear call history | ||
mock.mockClear(); // Clears calls but keeps implementation | ||
All mock types support these basic options: | ||
// Reset completely | ||
mock.mockReset(); // Clears everything | ||
- `debug`: Enable detailed logging (default: false) | ||
- `asyncTimeout`: Maximum time for async operations (default: 5000ms) | ||
// Restore original | ||
mock.mockRestore(); // Restores original implementation | ||
### Function Mock Options | ||
// Access state | ||
mock.calls.all(); // Call arguments with context | ||
mock.calls.count(); // Number of calls | ||
``` | ||
- `trackCalls`: Record arguments and return values (default: false) | ||
- `autoSpy`: Create spies for function properties (default: false) | ||
### Object Mock Options | ||
- `inplace`: Modify original object or create new one (default: false) | ||
- `prototypeChain`: Deep clone and mock prototype chain (default: true) | ||
- `overrides`: Override specific properties or methods | ||
### Class Mock Options | ||
- `inplace`: Modify original class or create new one (default: false) | ||
- `preservePrototype`: Preserve prototype chain (default: true) | ||
- `preserveConstructor`: Preserve constructor behavior (default: true) | ||
- `mockStatic`: Mock static members (default: false) | ||
- `overrides`: Override specific methods | ||
## Advanced Usage | ||
### Pattern Matching | ||
Control which methods to mock using glob patterns: | ||
```typescript | ||
const service = mock.obj<Service>( | ||
{ | ||
getData: () => Promise.resolve({data: 'test'}), | ||
getDataAsync: () => Promise.resolve({data: 'test'}), | ||
setData: data => {}, | ||
setDataAsync: async data => {}, | ||
}, | ||
{ | ||
patterns: [ | ||
'get*', // Mock all getter methods | ||
'!setData', // Exclude setData method | ||
], | ||
}, | ||
); | ||
``` | ||
### Async Mocking | ||
@@ -644,3 +371,3 @@ | ||
// Using replace for method replacement | ||
// Using replace | ||
mock.replace(service, 'getUser', async id => ({ | ||
@@ -651,3 +378,3 @@ id, | ||
// Using overrides for partial implementation | ||
// Using overrides | ||
const partialService = mock.obj<UserService>( | ||
@@ -663,335 +390,100 @@ {}, | ||
## Best Practices | ||
## Debugging | ||
1. **Reset Between Tests** | ||
### Debug Logging | ||
```typescript | ||
beforeEach(() => { | ||
mockFn.mockReset(); | ||
// or | ||
mockObj.mockReset(); | ||
}); | ||
``` | ||
Enable detailed logging for troubleshooting: | ||
2. **Type Safety** | ||
```typescript | ||
// Prefer interfaces for better type inference | ||
interface Service { | ||
method(): string; | ||
} | ||
const mock = mock.obj<Service>(); | ||
``` | ||
3. **Error Handling** | ||
```typescript | ||
// Always test error cases | ||
api.fetch.mockRejectedValue(new Error('Network error')); | ||
await expect(api.fetch()).rejects.toThrow('Network error'); | ||
``` | ||
4. **Verification** | ||
```typescript | ||
// Verify call count and arguments | ||
expect(mockFn.calls.count()).toBe(1); | ||
expect(mockFn.calls.all()[0].args).toEqual(['expected arg']); | ||
``` | ||
5. **Using inPlace** | ||
```typescript | ||
// For objects: Use inPlace when you need to maintain object references | ||
const mockObj = mock.obj(original, { | ||
inPlace: true, | ||
overrides: { | ||
method: () => 'mocked', | ||
}, | ||
}); | ||
// For classes: Be cautious with inPlace as it affects the original class | ||
const MockClass = mock.cls(Original); // Creates new class by default | ||
// If you need to modify the original class: | ||
const ModifiedClass = mock.cls(Original, { | ||
inPlace: true, | ||
// Remember to restore if needed | ||
}); | ||
``` | ||
## Troubleshooting | ||
### Common Issues | ||
1. **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`) | ||
2. **Type Inference Issues** | ||
- Use explicit type parameters with interfaces | ||
- Define complete interfaces for complex types | ||
- Use `as MockFunction<T>` for better type support | ||
3. **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) | ||
4. **inPlace Issues** | ||
- For objects: Use `inPlace: true` if you need to maintain object references | ||
- For classes: Be aware that `inPlace: true` modifies the original class | ||
- Remember to restore mocked objects/classes if needed: | ||
```typescript | ||
const mock = mock.obj(original, {inPlace: true}); | ||
// ... use mock ... | ||
mock.restore(); // Restore original implementation | ||
``` | ||
- When using `inPlace` with inheritance, ensure proper super calls are preserved | ||
- Consider using a new instance if you don't specifically need to modify the original | ||
## Contributing | ||
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. | ||
## License | ||
Apache-2.0 - see [LICENSE](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: | ||
```typescript | ||
// Create a new mock object (default behavior) | ||
const mockObj = mock.obj(original, { | ||
overrides: { | ||
method: () => 'mocked', | ||
}, | ||
}); | ||
import {debug} from '@corez/mock'; | ||
// Modify the original object | ||
const modifiedObj = mock.obj(original, { | ||
inPlace: true, | ||
overrides: { | ||
method: () => 'mocked', | ||
}, | ||
}); | ||
// Enable debug logging | ||
debug.enable(); | ||
// Deep mocking is enabled by default | ||
const deepMockObj = mock.obj(original, { | ||
prototypeChain: true, // this is default | ||
}); | ||
``` | ||
// Set log level | ||
debug.setLevel('error'); // 'error' | 'warn' | 'info' | 'debug' | ||
### 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: | ||
```typescript | ||
// Create a new mock class (default behavior) | ||
const MockClass = mock.cls(Original, { | ||
static: true, | ||
overrides: { | ||
method: () => 'mocked', | ||
}, | ||
// Custom handler | ||
debug.setHandler((level, message) => { | ||
console.log(`[MOCK] ${level}: ${message}`); | ||
}); | ||
// Modify the original class | ||
const ModifiedClass = mock.cls(Original, { | ||
inPlace: true, | ||
static: true, | ||
overrides: { | ||
method: () => 'mocked', | ||
}, | ||
}); | ||
``` | ||
### Cast Mock Options | ||
### Call Tracking | ||
Options for type casting and partial implementations: | ||
Track and verify method calls: | ||
- `keepPrototype`: Maintains the prototype chain when casting objects (default: true) | ||
## Usage Examples | ||
### Basic Function Mocking | ||
```typescript | ||
import {mock} from '@corez/mock'; | ||
// Clear history | ||
mock.mockClear(); // Clears calls but keeps implementation | ||
// Create a mock function | ||
const mockFn = mock.fn<() => string>(); | ||
mockFn.mockReturnValue('hello'); | ||
// Reset completely | ||
mock.mockReset(); // Clears everything | ||
// Track calls | ||
expect(mockFn()).toBe('hello'); | ||
expect(mockFn.mock.calls.length).toBe(1); | ||
``` | ||
// Restore original | ||
mock.mockRestore(); // Restores original implementation | ||
### Object Mocking | ||
```typescript | ||
import {mock} from '@corez/mock'; | ||
interface User { | ||
name: string; | ||
getId(): number; | ||
} | ||
// Create a mock object | ||
const mockUser = mock.obj<User>({ | ||
name: 'Test User', | ||
getId: () => 1, | ||
}); | ||
// Override methods | ||
mockUser.getId.mockReturnValue(2); | ||
// Access state | ||
mock.calls.all(); // Call arguments with context | ||
mock.calls.count(); // Number of calls | ||
``` | ||
### Class Mocking | ||
## Best Practices | ||
```typescript | ||
import {mock} from '@corez/mock'; | ||
1. **Type Safety** | ||
class Database { | ||
connect() { | ||
/* ... */ | ||
} | ||
query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
- Always provide type parameters | ||
- Use interfaces for mock shapes | ||
- Let TypeScript infer return types | ||
// Create a mock class | ||
const MockDatabase = mock.cls(Database, { | ||
mockStatic: true, | ||
preserveConstructor: true, | ||
}); | ||
2. **Pattern Matching** | ||
const db = new MockDatabase(); | ||
db.query.mockResolvedValue({rows: []}); | ||
``` | ||
- Use specific patterns for needed methods | ||
- Combine positive and negative patterns | ||
- Group related methods with patterns | ||
### Type Casting | ||
3. **Performance** | ||
```typescript | ||
import {mock} from '@corez/mock'; | ||
- Disable prototype chain mocking when not needed | ||
- Use shallow cloning for simple objects | ||
- Clean up mocks after use | ||
interface ComplexType { | ||
data: string; | ||
process(): Promise<void>; | ||
} | ||
4. **Error Handling** | ||
- Test error cases | ||
- Verify error messages | ||
- Handle async errors properly | ||
// Cast partial implementation to full mock | ||
const partial = {data: 'test'}; | ||
const mockObj = mock.cast<ComplexType>(partial); | ||
``` | ||
## Troubleshooting | ||
## Mock Composition | ||
### Common Issues | ||
### Compose Function Options | ||
1. **Mock Not Tracking Calls** | ||
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. | ||
- Enable `trackCalls` for functions | ||
- Method calls tracked automatically for objects/classes | ||
- Check mock property access | ||
#### Options Structure | ||
2. **Type Inference Issues** | ||
```typescript | ||
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; | ||
}; | ||
}); | ||
``` | ||
- Use explicit type parameters | ||
- Define complete interfaces | ||
- Use `as MockFunction<T>` for better typing | ||
#### Design Decisions | ||
3. **Prototype Chain Issues** | ||
1. **Replace vs Override** | ||
- Use `preservePrototype: true` for classes | ||
- Use `prototypeChain: true` for objects | ||
- Use `keepPrototype: true` for casting | ||
- `replace` is specifically designed for object mocking, allowing direct method replacement | ||
- `override` is used in class mocking for overriding prototype methods | ||
- While they may seem similar, they serve different purposes and contexts | ||
4. **Circular References** | ||
- Use `WeakSet` to track processed objects | ||
- Enable debug logging | ||
- Consider shallow cloning | ||
2. **Type Safety** | ||
## Contributing | ||
- 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 | ||
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. | ||
3. **Usage Examples** | ||
## License | ||
```typescript | ||
// Class mocking | ||
class Service { | ||
getData(): string { | ||
return 'data'; | ||
} | ||
} | ||
const mockService = compose(Service, { | ||
override: { | ||
getData: () => 'mocked', | ||
}, | ||
}); | ||
// Object mocking with replace | ||
const obj = { | ||
method: () => 'original', | ||
}; | ||
const mockObj = compose(obj, { | ||
replace: { | ||
method: () => 'replaced', | ||
}, | ||
}); | ||
``` | ||
4. **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` | ||
Apache-2.0 - see [LICENSE](LICENSE) for details. |
@@ -28,3 +28,3 @@ import {BaseMockOptions, CastMockOptions, ClassMockOptions, FunctionMockOptions, ObjectMockOptions} from './types'; | ||
...DEFAULT_BASE_OPTIONS, | ||
inPlace: false, | ||
inplace: false, | ||
prototypeChain: true, | ||
@@ -40,6 +40,7 @@ overrides: {}, | ||
...DEFAULT_BASE_OPTIONS, | ||
inPlace: false, | ||
inplace: false, | ||
preservePrototype: true, | ||
preserveConstructor: true, | ||
static: false, | ||
static: true, | ||
inheritance: true, | ||
overrides: {}, | ||
@@ -46,0 +47,0 @@ }; |
@@ -9,3 +9,2 @@ import { | ||
import {cast, cls, compose, fn, obj, replace} from './mocks'; | ||
import {factory} from './mocks/factory'; | ||
import {MockRegistry} from './registry'; | ||
@@ -107,6 +106,2 @@ import { | ||
} | ||
factory<T extends Constructor<any>>(target: T, options: ObjectMockOptions = {}): T { | ||
return factory(target, options); | ||
} | ||
} | ||
@@ -150,4 +145,2 @@ | ||
) => globalMock.compose(target, _options), | ||
factory: <T extends Constructor<any>>(target: T, options: ObjectMockOptions = {}) => | ||
globalMock.factory(target, options), | ||
}, | ||
@@ -154,0 +147,0 @@ ) as MockFn; |
@@ -18,3 +18,3 @@ import {cls} from '../class'; | ||
const MockedClass = cls(CallbackURL, {inPlace: true}); | ||
const MockedClass = cls(CallbackURL, {inplace: true}); | ||
const instance = new MockedClass(); | ||
@@ -21,0 +21,0 @@ |
@@ -209,3 +209,3 @@ import type {ClsMock, MockFunction} from '../../types'; | ||
it('should handle inheritance correctly when inPlace is true', () => { | ||
it('should handle inheritance correctly when inplace is true', () => { | ||
class ExtendedChild extends Base { | ||
@@ -229,3 +229,3 @@ constructor() { | ||
const MockChild = cls(ExtendedChild, {inPlace: true}); | ||
const MockChild = cls(ExtendedChild, {inplace: true}); | ||
const instance = new MockChild(); | ||
@@ -253,3 +253,3 @@ | ||
it('should handle static inheritance when inPlace is true', () => { | ||
it('should handle static inheritance when inplace is true', () => { | ||
class StaticBase { | ||
@@ -282,3 +282,3 @@ protected static states = new Map<any, any>(); | ||
const MockStaticChild = cls(StaticChild, {inPlace: true, static: true}); | ||
const MockStaticChild = cls(StaticChild, {inplace: true, static: true}); | ||
@@ -302,3 +302,3 @@ // Verify static inheritance works | ||
it('should handle super calls correctly when inPlace is true', () => { | ||
it('should handle super calls correctly when inplace is true', () => { | ||
class SuperBase { | ||
@@ -339,3 +339,3 @@ protected value: string; | ||
const MockChild = cls(SuperChild, {inPlace: true}); | ||
const MockChild = cls(SuperChild, {inplace: true}); | ||
const instance = new MockChild('test'); | ||
@@ -370,12 +370,157 @@ | ||
describe('selective mocking', () => { | ||
it('should allow mocking specific methods', () => { | ||
const MockAnimal = cls(Animal); | ||
const mockAnimal = new MockAnimal('test'); | ||
const mockSound = jest.fn(() => 'mocked sound'); | ||
class TestService { | ||
private value = 'test'; | ||
(mockAnimal.makeSound as MockFunction<() => string>).mockImplementation(mockSound); | ||
expect(mockAnimal.makeSound()).toBe('mocked sound'); | ||
expect(mockSound).toHaveBeenCalled(); | ||
expect(mockAnimal.name).toBe('test'); // Original getter behavior | ||
getValue() { | ||
return this.value; | ||
} | ||
setValue(value: string) { | ||
this.value = value; | ||
} | ||
getValueAsync() { | ||
return Promise.resolve(this.value); | ||
} | ||
_internalMethod() { | ||
return 'internal'; | ||
} | ||
static staticValue = 'static'; | ||
static getStaticValue() { | ||
return this.staticValue; | ||
} | ||
static setStaticValue(value: string) { | ||
this.staticValue = value; | ||
} | ||
static _internalStatic() { | ||
return 'internal static'; | ||
} | ||
} | ||
it('should allow mocking specific methods using patterns', () => { | ||
const MockClass = cls(TestService, { | ||
patterns: ['get*', '!_*'], | ||
}); | ||
const instance = new MockClass(); | ||
// Should mock getValue and getValueAsync | ||
expect(instance.getValue).toHaveProperty('mock'); | ||
expect(instance.getValueAsync).toHaveProperty('mock'); | ||
// Should not mock _internalMethod | ||
expect(instance._internalMethod()).toBe('internal'); | ||
// Should not mock setValue (not matched by pattern) | ||
expect(instance.setValue('new')).toBeUndefined(); | ||
expect(instance.getValue()).toBe('new'); | ||
}); | ||
it('should support negation patterns', () => { | ||
const MockClass = cls(TestService, { | ||
patterns: ['*', '!set*', '!_*'], | ||
}); | ||
const instance = new MockClass(); | ||
// Should mock getValue and getValueAsync | ||
expect(instance.getValue).toHaveProperty('mock'); | ||
expect(instance.getValueAsync).toHaveProperty('mock'); | ||
// Should not mock setValue and _internalMethod | ||
expect(instance.setValue('new')).toBeUndefined(); | ||
expect(instance._internalMethod()).toBe('internal'); | ||
}); | ||
it('should support static method patterns', () => { | ||
const MockClass = cls(TestService, { | ||
static: ['get*', '!_*'], | ||
}); | ||
// Should mock getStaticValue | ||
expect(MockClass.getStaticValue).toHaveProperty('mock'); | ||
// Should not mock _internalStatic | ||
expect(MockClass._internalStatic()).toBe('internal static'); | ||
// Should not mock setStaticValue | ||
expect(MockClass.setStaticValue('new')).toBeUndefined(); | ||
expect(MockClass.staticValue).toBe('new'); | ||
}); | ||
it('should support boolean static option', () => { | ||
const MockClass = cls(TestService, { | ||
static: true, | ||
}); | ||
// Should mock all static methods | ||
expect(MockClass.getStaticValue).toHaveProperty('mock'); | ||
expect(MockClass.setStaticValue).toHaveProperty('mock'); | ||
expect(MockClass._internalStatic).toHaveProperty('mock'); | ||
}); | ||
it('should not mock any static methods when static is false', () => { | ||
const MockClass = cls(TestService, { | ||
static: false, | ||
}); | ||
// Should not mock any static methods | ||
expect(MockClass.getStaticValue()).toBe('static'); | ||
expect(MockClass.setStaticValue('new')).toBeUndefined(); | ||
expect(MockClass._internalStatic()).toBe('internal static'); | ||
}); | ||
it('should respect inheritance option', () => { | ||
class Parent { | ||
parentMethod() { | ||
return 'parent'; | ||
} | ||
} | ||
class Child extends Parent { | ||
childMethod() { | ||
return 'child'; | ||
} | ||
} | ||
const MockClass = cls(Child, { | ||
inheritance: false, | ||
}); | ||
const instance = new MockClass(); | ||
// Should mock child method | ||
expect(instance.childMethod).toHaveProperty('mock'); | ||
// Should not mock parent method | ||
expect(instance.parentMethod()).toBe('parent'); | ||
}); | ||
it('should handle complex pattern combinations', () => { | ||
const MockClass = cls(TestService, { | ||
// Mock all get* methods except internal ones | ||
patterns: ['get*', '!_*'], | ||
// Mock all static get* methods except internal ones | ||
static: ['get*', '!_*'], | ||
// Don't mock inherited methods | ||
inheritance: false, | ||
}); | ||
const instance = new MockClass(); | ||
// Instance methods | ||
expect(instance.getValue).toHaveProperty('mock'); | ||
expect(instance.getValueAsync).toHaveProperty('mock'); | ||
expect(instance._internalMethod()).toBe('internal'); | ||
expect(instance.setValue('new')).toBeUndefined(); | ||
// Static methods | ||
expect(MockClass.getStaticValue).toHaveProperty('mock'); | ||
expect(MockClass._internalStatic()).toBe('internal static'); | ||
expect(MockClass.setStaticValue('new')).toBeUndefined(); | ||
}); | ||
}); | ||
@@ -411,3 +556,3 @@ | ||
describe('inPlace option', () => { | ||
describe('inplace option', () => { | ||
class TestClass { | ||
@@ -423,4 +568,4 @@ value: string = 'original'; | ||
it('should modify original class when inPlace is true', () => { | ||
const ModifiedClass = cls(TestClass, {inPlace: true}); | ||
it('should modify original class when inplace is true', () => { | ||
const ModifiedClass = cls(TestClass, {inplace: true}); | ||
@@ -446,4 +591,4 @@ // Verify returned class is the same as original | ||
it('should mark mock class with IS_MOCKED symbol when inPlace is true', () => { | ||
const MockClass = cls(TestClass, {inPlace: true}); | ||
it('should mark mock class with IS_MOCKED symbol when inplace is true', () => { | ||
const MockClass = cls(TestClass, {inplace: true}); | ||
expect((MockClass as ClsMock<typeof TestClass>).__is_mocked__).toBe(true); | ||
@@ -466,4 +611,4 @@ }); | ||
it('should mark mock class with IS_MOCKED symbol when inPlace is true', () => { | ||
const MockClass = cls(TestClass, {inPlace: true}); | ||
it('should mark mock class with IS_MOCKED symbol when inplace is true', () => { | ||
const MockClass = cls(TestClass, {inplace: true}); | ||
expect((MockClass as ClsMock<typeof TestClass>).__is_mocked__).toBe(true); | ||
@@ -470,0 +615,0 @@ }); |
@@ -73,5 +73,5 @@ import {IS_MOCKED} from '../../constants/mock-symbols'; | ||
it('should modify original when inPlace is true', () => { | ||
it('should modify original when inplace is true', () => { | ||
const _original = new TestClass('test'); | ||
const mock = obj(_original, {inPlace: true}); | ||
const mock = obj(_original, {inplace: true}); | ||
expect(mock).toBe(_original); | ||
@@ -82,3 +82,3 @@ }); | ||
const _original = new TestClass('test'); | ||
const mock = obj(_original, {inPlace: false, prototypeChain: false}); | ||
const mock = obj(_original, {inplace: false, prototypeChain: false}); | ||
mock.increment(); | ||
@@ -118,5 +118,5 @@ expect(_original.getCounter()).toBe(0); // Original should be unchanged | ||
it('should support super calls and modify original when inPlace is true', () => { | ||
it('should support super calls and modify original when inplace is true', () => { | ||
const child = new Child(); | ||
const mock = obj(child, {inPlace: true}); | ||
const mock = obj(child, {inplace: true}); | ||
expect(mock.getValue()).toBe('child:parent'); | ||
@@ -127,5 +127,5 @@ expect(mock.childMethod()).toBe('child:base'); | ||
it('should support super calls and create new instance when inPlace is false', () => { | ||
it('should support super calls and create new instance when inplace is false', () => { | ||
const child = new Child(); | ||
const mock = obj(child, {inPlace: false}); | ||
const mock = obj(child, {inplace: false}); | ||
expect(mock.getValue()).toBe('child:parent'); | ||
@@ -298,5 +298,5 @@ expect(mock.childMethod()).toBe('child:base'); | ||
it('should handle class mock with inPlace', () => { | ||
it('should handle class mock with inplace', () => { | ||
const _original = new TestClass(); | ||
const MockTestClass = cls(TestClass, {inPlace: true}); | ||
const MockTestClass = cls(TestClass, {inplace: true}); | ||
const instance = new MockTestClass(); | ||
@@ -322,2 +322,67 @@ const objectMock = obj(instance); | ||
}); | ||
describe('pattern matching', () => { | ||
class TestService { | ||
getValue() { | ||
return 'value'; | ||
} | ||
setValue(value: string) { | ||
return value; | ||
} | ||
protected _internalMethod() { | ||
return 'internal'; | ||
} | ||
getValueAsync() { | ||
return Promise.resolve('async value'); | ||
} | ||
callInternalMethod() { | ||
return this._internalMethod(); | ||
} | ||
} | ||
it('should mock methods matching patterns', () => { | ||
const service = new TestService(); | ||
const mock = obj(service, { | ||
patterns: ['get*'], | ||
prototypeChain: true, | ||
}); | ||
expect(mock.getValue).toHaveProperty('mock'); | ||
expect(mock.getValueAsync).toHaveProperty('mock'); | ||
expect(typeof mock.setValue).toBe('function'); | ||
expect(mock.setValue('test')).toBe('test'); | ||
}); | ||
it('should exclude methods matching negative patterns', () => { | ||
const service = new TestService(); | ||
const mock = obj(service, { | ||
patterns: ['*', '!set*', '!_*'], | ||
prototypeChain: true, | ||
}); | ||
expect(mock.getValue).toHaveProperty('mock'); | ||
expect(mock.getValueAsync).toHaveProperty('mock'); | ||
expect(typeof mock.setValue).toBe('function'); | ||
expect(mock.setValue('test')).toBe('test'); | ||
expect(mock.callInternalMethod()).toBe('internal'); | ||
}); | ||
it('should support complex pattern combinations', () => { | ||
const service = new TestService(); | ||
const mock = obj(service, { | ||
patterns: ['get*', '!_*'], | ||
prototypeChain: true, | ||
}); | ||
expect(mock.getValue).toHaveProperty('mock'); | ||
expect(mock.getValueAsync).toHaveProperty('mock'); | ||
expect(typeof mock.setValue).toBe('function'); | ||
expect(mock.setValue('test')).toBe('test'); | ||
expect(mock.callInternalMethod()).toBe('internal'); | ||
}); | ||
}); | ||
}); |
@@ -1,43 +0,13 @@ | ||
import {IS_MOCKED, MOCK_SYMBOLS} from '../constants/mock-symbols'; | ||
import {IS_MOCKED} from '../constants/mock-symbols'; | ||
import {DEFAULT_CLASS_OPTIONS} from '../defaults'; | ||
import {createSpy} from '../spy'; | ||
import {ClassMockOptions, ClsMock, Constructor} from '../types'; | ||
import {createDebugContext} from '../utils/debug'; | ||
import {createMockConstructor, setupMockClassPrototype} from '../utils/mock-constructor'; | ||
import {mockMethods} from '../utils/mock-descriptors'; | ||
import {handleStaticMethodInheritance} from '../utils/mock-method-filters'; | ||
import {markAsMocked} from '../utils/mock-state'; | ||
function handleStaticMethodInheritance( | ||
mockClass: any, | ||
originalClass: any, | ||
_options: Partial<ClassMockOptions> = {}, | ||
): void { | ||
let currentProto = Object.getPrototypeOf(originalClass); | ||
while (currentProto && currentProto !== Function.prototype) { | ||
Object.getOwnPropertyNames(currentProto) | ||
.filter(prop => typeof currentProto[prop] === 'function') | ||
.forEach(methodName => { | ||
if (!mockClass[methodName]) { | ||
const method = currentProto[methodName].bind(mockClass); | ||
mockClass[methodName] = createSpy(method); | ||
} | ||
}); | ||
currentProto = Object.getPrototypeOf(currentProto); | ||
} | ||
} | ||
function initializeMockInstance(instance: any, config: ClassMockOptions): void { | ||
// Use symbols for all mock-related properties | ||
instance[MOCK_SYMBOLS.STATES] = new Map(); | ||
instance[MOCK_SYMBOLS.STATE_KEY] = Symbol('state'); | ||
instance[MOCK_SYMBOLS.INSTANCE_ID] = Symbol('instance'); | ||
instance[MOCK_SYMBOLS.CONFIG] = config; | ||
if (config.debug) { | ||
instance[MOCK_SYMBOLS.DEBUG_CONTEXT] = createDebugContext(config); | ||
} | ||
} | ||
/** | ||
* Creates a mock class with all methods and properties mocked while preserving the original class structure. | ||
* By default, it returns a new mock class. | ||
* Can optionally modify the original class if inPlace option is set to true. | ||
* Can optionally modify the original class if inplace option is set to true. | ||
* | ||
@@ -74,2 +44,4 @@ * Key features: | ||
...DEFAULT_CLASS_OPTIONS, | ||
// When inplace is true, enable static method mocking by default | ||
static: options.inplace ? true : DEFAULT_CLASS_OPTIONS.static, | ||
...options, | ||
@@ -82,78 +54,17 @@ }; | ||
if (config.inPlace) { | ||
// Modify the original class (when inPlace is true) | ||
// Mark the class as mocked | ||
Object.defineProperties(target, { | ||
[IS_MOCKED]: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
__is_mocked__: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
}); | ||
if (config.inplace) { | ||
// Modify the original class (when inplace is true) | ||
markAsMocked(target); | ||
markAsMocked(target.prototype); | ||
// Mock prototype methods | ||
Object.getOwnPropertyNames(target.prototype).forEach(prop => { | ||
if (prop === 'constructor') return; | ||
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, prop); | ||
if (descriptor) { | ||
const override = config.overrides?.[prop]; | ||
if (override) { | ||
Object.defineProperty(target.prototype, prop, { | ||
...descriptor, | ||
value: createSpy(override), | ||
}); | ||
} else if (typeof descriptor.value === 'function') { | ||
Object.defineProperty(target.prototype, prop, { | ||
...descriptor, | ||
value: config.preservePrototype ? createSpy(descriptor.value) : createSpy(), | ||
}); | ||
} | ||
} | ||
}); | ||
mockMethods(target.prototype, target.prototype, config, config.patterns); | ||
// Mark prototype as mocked | ||
Object.defineProperties(target.prototype, { | ||
[IS_MOCKED]: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
__is_mocked__: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
}); | ||
// Mock static methods | ||
Object.getOwnPropertyNames(target).forEach(prop => { | ||
if (prop === 'length' || prop === 'prototype' || prop === 'name') return; | ||
const descriptor = Object.getOwnPropertyDescriptor(target, prop); | ||
if (descriptor) { | ||
const override = config.overrides?.[prop]; | ||
if (override) { | ||
Object.defineProperty(target, prop, { | ||
...descriptor, | ||
value: createSpy(override), | ||
}); | ||
} else if (typeof descriptor.value === 'function') { | ||
Object.defineProperty(target, prop, { | ||
...descriptor, | ||
value: config.preservePrototype ? createSpy(descriptor.value) : createSpy(), | ||
}); | ||
} | ||
} | ||
}); | ||
if (config.static !== false) { | ||
mockMethods(target, target, config, Array.isArray(config.static) ? config.static : undefined); | ||
} | ||
// Handle static methods inheritance | ||
handleStaticMethodInheritance(target, target, {debug: config.debug}); | ||
// Handle static methods inheritance with updated config | ||
handleStaticMethodInheritance(target, target, config); | ||
@@ -164,128 +75,19 @@ return target as unknown as ClsMock<T>; | ||
// Create a new mock class (default behavior) | ||
const MockClass = function (this: any, ...args: any[]) { | ||
// Handle constructor call without new | ||
if (!(this instanceof MockClass)) { | ||
return new (MockClass as any)(...args); | ||
} | ||
const MockClass = createMockConstructor(target, config); | ||
// Create instance with proper prototype | ||
const instance = Object.create(target.prototype); | ||
Object.setPrototypeOf(instance, MockClass.prototype); | ||
// Set up prototype chain and mark as mocked | ||
setupMockClassPrototype(MockClass, target); | ||
// Initialize mock properties using symbols | ||
initializeMockInstance(instance, config); | ||
// Mark instance as mocked | ||
Object.defineProperties(instance, { | ||
[IS_MOCKED]: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
__is_mocked__: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
}); | ||
// Call original constructor if preserving constructor | ||
if (config.preserveConstructor) { | ||
const constructedInstance = Reflect.construct(target, args); | ||
// Copy instance properties | ||
Object.getOwnPropertyNames(constructedInstance).forEach(prop => { | ||
if (!(prop in instance)) { | ||
const descriptor = Object.getOwnPropertyDescriptor(constructedInstance, prop); | ||
if (descriptor) { | ||
Object.defineProperty(instance, prop, descriptor); | ||
} | ||
} | ||
}); | ||
} | ||
return instance; | ||
}; | ||
// Set up prototype chain | ||
Object.setPrototypeOf(MockClass, target); | ||
Object.setPrototypeOf(MockClass.prototype, target.prototype); | ||
// Mark mock class as mocked | ||
Object.defineProperties(MockClass, { | ||
[IS_MOCKED]: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
__is_mocked__: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
}); | ||
// Mark mock class prototype as mocked | ||
Object.defineProperties(MockClass.prototype, { | ||
[IS_MOCKED]: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
__is_mocked__: { | ||
value: true, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}, | ||
}); | ||
// Mock prototype methods | ||
Object.getOwnPropertyNames(target.prototype).forEach(prop => { | ||
if (prop === 'constructor') return; | ||
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, prop); | ||
if (descriptor) { | ||
const override = config.overrides?.[prop]; | ||
if (override) { | ||
Object.defineProperty(MockClass.prototype, prop, { | ||
...descriptor, | ||
value: createSpy(override), | ||
}); | ||
} else if (typeof descriptor.value === 'function') { | ||
Object.defineProperty(MockClass.prototype, prop, { | ||
...descriptor, | ||
value: config.preservePrototype ? createSpy(descriptor.value) : createSpy(), | ||
}); | ||
} | ||
} | ||
}); | ||
mockMethods(MockClass.prototype, target.prototype, config, config.patterns); | ||
// Mock static methods | ||
Object.getOwnPropertyNames(target).forEach(prop => { | ||
if (prop === 'length' || prop === 'prototype' || prop === 'name') return; | ||
const descriptor = Object.getOwnPropertyDescriptor(target, prop); | ||
if (descriptor) { | ||
const override = config.overrides?.[prop]; | ||
if (override) { | ||
Object.defineProperty(MockClass, prop, { | ||
...descriptor, | ||
value: createSpy(override), | ||
}); | ||
} else if (typeof descriptor.value === 'function') { | ||
Object.defineProperty(MockClass, prop, { | ||
...descriptor, | ||
value: config.preservePrototype ? createSpy(descriptor.value) : createSpy(), | ||
}); | ||
} | ||
} | ||
}); | ||
if (config.static !== false) { | ||
mockMethods(MockClass, target, config, Array.isArray(config.static) ? config.static : undefined); | ||
} | ||
// Handle static methods inheritance | ||
handleStaticMethodInheritance(MockClass, target, {debug: config.debug}); | ||
// Handle static methods inheritance with updated config | ||
handleStaticMethodInheritance(MockClass, target, config); | ||
return MockClass as unknown as ClsMock<T>; | ||
} |
@@ -1,51 +0,32 @@ | ||
import rfdc from 'rfdc'; | ||
import {IS_MOCKED} from '../constants/mock-symbols'; | ||
import {createSpy} from '../spy'; | ||
import {Fn, MockObject, ObjectMockOptions} from '../types'; | ||
import {cloneDeep, shallowClone} from '../utils/clone'; | ||
import {isMocked} from '../utils/mock-handlers'; | ||
import {createMethodSpy, mockObjectMethods} from '../utils/mock-methods'; | ||
const clone = rfdc({circles: true, proto: true}); | ||
/** | ||
* Creates a shallow clone of an object while preserving its prototype chain | ||
*/ | ||
function shallowClone<T extends object>(obj: T): T { | ||
const clone = Object.create(Object.getPrototypeOf(obj)); | ||
Object.getOwnPropertyNames(obj).forEach(key => { | ||
const descriptor = Object.getOwnPropertyDescriptor(obj, key); | ||
if (descriptor) { | ||
Object.defineProperty(clone, key, descriptor); | ||
} | ||
}); | ||
return clone; | ||
} | ||
/** | ||
* Creates a spy for a method while preserving its this binding | ||
*/ | ||
function createMethodSpy<T extends object>(method: Fn, mockTarget: T, isArrowFn: boolean): MockObject<Fn> { | ||
const spy = createSpy(); | ||
const boundMethod = function (this: T, ...args: unknown[]) { | ||
return method.apply(isArrowFn ? mockTarget : this, args); | ||
}; | ||
spy.mockImplementation(boundMethod); | ||
return spy; | ||
} | ||
/** | ||
* Creates a mock object by replacing methods with spies and tracking properties. | ||
* Can either modify the original object or create a clone. | ||
* - Can modify the original object or create a clone | ||
* - Supports method pattern matching for selective mocking | ||
* - Handles prototype chain methods if enabled | ||
* - Allows method and property overrides | ||
* - Preserves non-method properties | ||
* - Prevents re-mocking of already mocked objects | ||
* | ||
* @template T - The object type to mock | ||
* @template T - The type of object to mock | ||
* @param target - The object to mock | ||
* @param options - Mock options including configuration | ||
* @returns A mock object that tracks interactions | ||
* @param options - Configuration options | ||
* @param options.inplace - Whether to modify the target object or create a clone | ||
* @param options.prototypeChain - Whether to mock methods from the prototype chain | ||
* @param options.patterns - Glob patterns for selecting which methods to mock | ||
* @param options.overrides - Object containing method and property overrides | ||
* @returns The mocked object | ||
* @throws {Error} If target is undefined | ||
*/ | ||
export function obj<T extends object>(target: T | undefined, options: ObjectMockOptions<T> = {}): MockObject<T> { | ||
if (!target) { | ||
return {} as MockObject<T>; | ||
throw new Error('Cannot mock undefined object'); | ||
} | ||
// Check if the object is already mocked | ||
// Return existing mock if already mocked | ||
if (isMocked(target)) { | ||
@@ -55,59 +36,17 @@ return target as MockObject<T>; | ||
// Check if the object is a class instance that has been mocked | ||
if ((target as any).constructor && (target as any).constructor[IS_MOCKED]) { | ||
return target as MockObject<T>; | ||
} | ||
// Create mock target | ||
const mockTarget = options.inplace ? target : options.prototypeChain ? cloneDeep(target) : shallowClone(target); | ||
let mockTarget: T; | ||
if (options.inPlace) { | ||
mockTarget = target; | ||
} else { | ||
mockTarget = options.prototypeChain ? clone(target) : shallowClone(target); | ||
} | ||
// Mark the object as mocked | ||
// Mark as mocked | ||
Object.defineProperty(mockTarget, IS_MOCKED, { | ||
value: true, | ||
writable: false, | ||
enumerable: false, | ||
configurable: false, | ||
writable: false, | ||
}); | ||
// Process instance methods and properties | ||
Object.entries(Object.getOwnPropertyDescriptors(target)).forEach(([key, descriptor]) => { | ||
if (typeof descriptor.value === 'function') { | ||
const method = descriptor.value; | ||
const isArrowFn = !method.prototype; | ||
const spy = createMethodSpy(method, mockTarget, isArrowFn); | ||
// Mock methods | ||
mockObjectMethods(mockTarget, target, options); | ||
Object.defineProperty(mockTarget, key, { | ||
...descriptor, | ||
value: spy, | ||
}); | ||
} | ||
}); | ||
// Mock prototype chain methods if doing deep mocking | ||
if (options.prototypeChain) { | ||
let proto = Object.getPrototypeOf(target); | ||
while (proto && proto !== Object.prototype) { | ||
Object.entries(Object.getOwnPropertyDescriptors(proto)).forEach(([key, descriptor]) => { | ||
// Only mock methods that haven't been mocked yet | ||
if (!Object.prototype.hasOwnProperty.call(mockTarget, key) && typeof descriptor.value === 'function') { | ||
const method = descriptor.value; | ||
const isArrowFn = !method.prototype; | ||
const spy = createMethodSpy(method, mockTarget, isArrowFn); | ||
Object.defineProperty(mockTarget, key, { | ||
...descriptor, | ||
value: spy, | ||
}); | ||
} | ||
}); | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
} | ||
// Apply overrides if provided | ||
// Apply overrides | ||
if (options.overrides) { | ||
@@ -117,4 +56,3 @@ Object.entries(options.overrides).forEach(([key, value]) => { | ||
if (typeof value === 'function') { | ||
const spy = createSpy(); | ||
spy.mockImplementation(value as Fn); | ||
const spy = createMethodSpy(value as Fn, mockTarget, !value.prototype); | ||
mockTarget[propertyKey] = spy as T[keyof T]; | ||
@@ -121,0 +59,0 @@ } else { |
@@ -8,2 +8,3 @@ import {Fn, MockFunction} from './types'; | ||
* such as call tracking, return value manipulation, and behavior verification. | ||
* It supports both Jest and Jasmine style mocking APIs. | ||
* | ||
@@ -180,6 +181,21 @@ * @template T - The type of function being spied on | ||
public getSpy(): MockFunction<T> { | ||
/** | ||
* Gets the spy function with all mock properties and methods attached. | ||
* The returned spy function includes: | ||
* - mock property for tracking calls, results, instances and contexts | ||
* - Jest-style methods (mockReturnValue, mockImplementation, etc.) | ||
* - Jasmine-style methods (and.returnValue, and.callFake, etc.) | ||
* - Common assertions (toHaveBeenCalled, toHaveBeenCalledWith, etc.) | ||
*/ | ||
getSpy(): MockFunction<T> { | ||
return this.spyFunction; | ||
} | ||
/** | ||
* Updates the spy's state by setting implementation, return value, or error. | ||
* When updating one state, it clears other conflicting states to maintain consistency. | ||
* For example, setting a return value will clear the implementation. | ||
* | ||
* @param updates - Object containing state updates | ||
*/ | ||
private updateState(updates: {implementation?: T; returnValue?: ReturnType<T>; error?: any}): void { | ||
@@ -213,4 +229,7 @@ // Reset all states | ||
* Creates a new spy function with optional implementation. | ||
* The spy includes both Jest and Jasmine style mocking APIs. | ||
* | ||
* @template T - The type of function to spy on | ||
* @param implementation - Optional implementation for the spy | ||
* @returns A spy function with mock tracking capabilities | ||
*/ | ||
@@ -224,3 +243,6 @@ export function createSpy<T extends Fn>(implementation?: T): MockFunction<T> { | ||
* Creates a spy for a property with getter and/or setter. | ||
* Both the getter and setter are spies with full mock tracking capabilities. | ||
* | ||
* @template T - The type of the property | ||
* @returns An object containing spy functions for get and set operations | ||
*/ | ||
@@ -227,0 +249,0 @@ export function createPropertySpy<T>(): { |
@@ -115,3 +115,3 @@ import {IS_MOCKED} from '../constants/mock-symbols'; | ||
*/ | ||
inPlace?: boolean; | ||
inplace?: boolean; | ||
@@ -154,6 +154,17 @@ /** | ||
* When false, only mocks the top level object's own methods | ||
* Only applies when inPlace is false | ||
* Only applies when inplace is false | ||
* @default false | ||
*/ | ||
prototypeChain?: boolean; | ||
/** | ||
* Glob patterns for methods/properties to mock | ||
* Supports ! prefix for exclusion | ||
* Examples: | ||
* - ['get*'] - mock all methods starting with 'get' | ||
* - ['!private*'] - exclude all methods starting with 'private' | ||
* - ['handle{Get,Post}*'] - mock all methods starting with handleGet or handlePost | ||
* @default undefined - mocks everything except constructor | ||
*/ | ||
patterns?: string[]; | ||
} | ||
@@ -181,7 +192,26 @@ | ||
/** | ||
* Controls whether to mock static class members | ||
* Includes static methods, properties, and getters/setters | ||
* Glob patterns for methods/properties to mock | ||
* Supports ! prefix for exclusion | ||
* Examples: | ||
* - ['get*'] - mock all methods starting with 'get' | ||
* - ['!private*'] - exclude all methods starting with 'private' | ||
* - ['handle{Get,Post}*'] - mock all methods starting with handleGet or handlePost | ||
* @default undefined - mocks everything except constructor | ||
*/ | ||
patterns?: string[]; | ||
/** | ||
* Controls static member mocking | ||
* Can be boolean to mock all/none static members | ||
* Or array of glob patterns with ! prefix for exclusion | ||
* @default false | ||
*/ | ||
static?: boolean; | ||
static?: boolean | string[]; | ||
/** | ||
* Controls whether to mock inherited methods | ||
* When false, only mocks methods defined directly on the class | ||
* @default true | ||
*/ | ||
inheritance?: boolean; | ||
} | ||
@@ -417,7 +447,2 @@ | ||
/** | ||
* Creates a factory-based mock class that automatically mocks instances on construction | ||
*/ | ||
factory<T extends Constructor<any>>(target: T, options?: ObjectMockOptions): T; | ||
/** | ||
* Casts a partial implementation to a complete mock | ||
@@ -424,0 +449,0 @@ */ |
@@ -64,3 +64,3 @@ import {IS_MOCKED} from '../../constants/mock-symbols'; | ||
it('should handle class mock with inPlace', () => { | ||
it('should handle class mock with inplace', () => { | ||
class TestClass { | ||
@@ -71,3 +71,3 @@ method() { | ||
} | ||
const MockTestClass = cls(TestClass, {inPlace: true}); | ||
const MockTestClass = cls(TestClass, {inplace: true}); | ||
expect(isMocked(MockTestClass)).toBe(true); | ||
@@ -74,0 +74,0 @@ |
@@ -25,2 +25,4 @@ import {BaseMockOptions} from '../types'; | ||
export type MockType = 'object' | 'function' | 'class'; | ||
/** | ||
@@ -95,3 +97,3 @@ * Debug context for mock operations | ||
*/ | ||
logMockCreation(type: 'function' | 'object' | 'class'): void { | ||
logMockCreation(type: MockType): void { | ||
this.log(`Created new ${type} mock`, DebugLevel.INFO); | ||
@@ -98,0 +100,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
1185505
63
14865
22
469
6