jasmine-mock-factory
Advanced tools
Comparing version 1.0.4 to 2.0.0
{ | ||
"name": "jasmine-mock-factory", | ||
"version": "1.0.4", | ||
"version": "2.0.0", | ||
"description": "A Jasmine helper for creating mocked classes", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
133
README.md
@@ -15,10 +15,31 @@ # Jasmine Mock Factory | ||
const mockInstance = MockFactory.create(SomeClass); | ||
mockInstance.doSomething.and.returnValue('awesome!'); | ||
/* arrange */ | ||
mockInstance._spy.doSomething._func.and.returnValue('awesome!'); | ||
/* act */ | ||
mockInstance.doSomething(); // returns 'awesome!' | ||
/* assert */ | ||
expect(mockInstance.doSomething).toHaveBeenCalled(); | ||
} | ||
``` | ||
## Quick reference | ||
```TypeScript | ||
/* create a mock from a class*/ | ||
const mockInstance1 = MockFactory.create(RealClass); | ||
/* create a mock from an instance*/ | ||
const mockInstance2 = MockFactory.create(realInstance); | ||
/* access a function spy */ | ||
const spy1 = mockInstance._spy.functionName._func | ||
/* access a getter spy */ | ||
const spy2 = mockInstance._spy.propertyName._get | ||
/* access a setter spy */ | ||
const spy3 = mockInstance._spy.propertyName._set | ||
``` | ||
## Prerequisite | ||
@@ -28,7 +49,7 @@ | ||
This util requires [ES6 Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and only contains un-compiled `*.ts` files which must be compiled with a [TypeScript](https://www.typescriptlang.org/) compiler. | ||
This util requires [ES6 Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) and only contains `*.ts` files that must be compiled with a [TypeScript](https://www.typescriptlang.org/) compiler. | ||
## Usage | ||
## Usage | ||
### Install | ||
@@ -67,31 +88,99 @@ ```Shell | ||
#### From window objects | ||
```TypeScript | ||
/* make sure you have included dom types from the TypeScript library */ | ||
const mockWindow = MockFactory.create(window); | ||
const mockDocument = MockFactory.create(document); | ||
const mockLocation = MockFactory.create(location); | ||
``` | ||
### Using a mock | ||
`MockFactory.create()` will return an object with the same interface as the original object. You can invoke methods and get/set properties on this object. | ||
* `MockFactory.create()` will return an object with the same interface as the real object. You can invoke functions and access properties on this object. | ||
* In addition, the mock object provides a `_spy` facade, where you can access and config spies on functions and properties. | ||
```TypeScript | ||
const mockInstance = MockFactory.create(location); | ||
mockInstance.reload(); // use it as if it were the real window.location | ||
let temp = mockInstance.search; // use it as if it were the real window.search | ||
mockInstance.hash = 'myHash'; // use it as if it were the real window.hash | ||
mockInstance._spy.reload._func; // returns the spy behind location.reload | ||
mockInstance._spy.search._get; // returns the spy behind the getter for location.search | ||
mockInstance._spy.hash._set; // returns the spy behind the setter for location.hash | ||
``` | ||
* All the public and private methods will have a jasmine.Spy as the initial value. The Spy cannot be overwritten. | ||
* All the public and private properties will have `undefined` as the initial value. The value can be overwritten with anything. | ||
### Examples | ||
#### Invoking functions | ||
* All functions will have a `jasmine.Spy` as the initial value. The spy cannot be overwritten and returns `undefined` by default. | ||
* To access protected and private functions, cast the mockInstance `as any` or use bracket notation. | ||
```TypeScript | ||
class RealClass { | ||
public doSomething(...arg: any[]) { ... } | ||
public someProperty = 'whatever'; | ||
} | ||
mockInstance.publicFunction(42); // the spy behind it is invoked with 42 | ||
const mockInstance = MockFactory.create(RealClass); | ||
(mockInstance as any).privateFunction(42); | ||
mockInstance['privateFunction'](42); // equivalent | ||
``` | ||
// get, set property | ||
expect(mockInstance.someProperty).toBeUndefined(); | ||
mockInstance.someProperty = 'hello'; | ||
expect(mockInstance.someProperty).toBe('hello'); | ||
#### Spying/stubbing functions | ||
* You can change return values of functions or assert their calls by accessing them directly or through the `_spy` facade. | ||
* Access a function spy on `mockInstance._spy.functionName._func`. | ||
```TypeScript | ||
/* stubbing a public function */ | ||
mockInstance._spy.publicFunction._func.and.returnValue(42); | ||
(mockInstance.publicFunction as jasmine.Spy).and.returnValue(42); // equivalent, but not recommented because it requires casting | ||
// use function spy | ||
expect(mockInstance.doSomething).not.toHaveBeenCalled(); | ||
/* stubbing a private function */ | ||
mockInstance._spy.privateFunction._func.and.returnValue(42); | ||
((mockInstance as any).privateFunction as jasmine.Spy).and.returnValue(42); // equivalent, but not recommented because it requires casting twice | ||
``` | ||
(mockInstance.doSomething as jasmine.Spy).and.returnValue('awesome!'); | ||
#### Accessing properties | ||
* All properties have `undefined` as the initial value. The value can be overwritten with anything. | ||
* You have read and write access to any property, even if they were readonly in the real object. | ||
* To read or write a protected or private property, cast the mockInstance `as any` or use bracket notation. | ||
* To write a readonly property, cast the mockInstance `as any`. The bracket notation won't work. | ||
* By default, modification to the properties will persist, even if a getter or setter exists in the real object. | ||
```TypeScript | ||
/* persist modification */ | ||
mockInstance.publicProperty = 42; | ||
let temp = mockInstance.publicProperty; // temp = 42; | ||
expect(mockInstance.doSomething(42)).toBe('awesome!'); | ||
expect(mockInstance.doSomething).toHaveBeenCalledWith(42); | ||
/* access readonly property */ | ||
mockInstance.readonlyProperty = 42; // typescript compiler error | ||
(mockInstance as any).readonlyProperty = 42; // no problem | ||
mockInstance['readonlyProperty'] = 42; // typescript compiler error | ||
/* access private property */ | ||
(mockInstance as any).privateProperty = 'foo'; | ||
mockInstance['privateProperty'] = 'foo'; // equivalent | ||
``` | ||
#### Spying/stubbing getters and setters | ||
* All properties have spies on the getter and setter, even if the getter and setter don't exist in the real object. | ||
* Access a getter spy on `mockInstance._spy.propertyName._get`. | ||
* Access a setter spy on `mockInstance._spy.propertyName._set`. | ||
* NOTE: modification to the properties will not persist after getter or setter spies are customized | ||
* NOTE: `expect(mockInstance.someProperty).toBe(...)` will trigger `mockInstance._spy.someProperty._get`. Design the sequence of your assertions carefully to avoid shooting yourself in the foot. | ||
```TypeScript | ||
/* assert getter calls */ | ||
let temp = mockInstance.publicProperty; | ||
expect(mockInstance._spy.publicProperty._get).toHaveBeenCalled(); | ||
/* assert setter calls on a public property */ | ||
mockInstance.publicProperty = 42; | ||
expect(mockInstance._spy.publicProperty._set).toHaveBeenCalledWith(42); | ||
/* customize setter */ | ||
expect(mockInstance.publicProperty).toBe(42); // pass. setter hasn't been customized | ||
mockInstance._spy.publicProperty._set.and.callFake(() => { /* noop */}); | ||
mockInstance.publicProperty = 100; | ||
expect(mockInstance.publicProperty).toBe(100); // fail. expect 42 to be 100. setter was customized | ||
/* assert setter calls on a private property */ | ||
mockInstance['privateProperty'] = 42; | ||
expect(mockInstance._spy.privateProperty._set).toHaveBeenCalledWith(42); | ||
/* customize getter */ | ||
expect(mockInstance['privateProperty']).toBe(42); // pass. getter hasn't been customized | ||
mockInstance._spy.privateProperty._get.and.returnValue(100); | ||
mockInstance['privateProperty'] = 42; | ||
expect(mockInstance['privateProperty']).toBe(42); // fail, expect 100 to be 42. getter was customzied | ||
``` | ||
## Develope | ||
@@ -98,0 +187,0 @@ This project is built with [Angular CLI](https://cli.angular.io/) |
@@ -1,38 +0,55 @@ | ||
interface Type<T> extends Function { | ||
new (...args: any[]): T; | ||
export declare type Mock<T> = T & SpyFacade<T>; | ||
export interface SpyFacade<T> { | ||
_spy: Spied<T> & SpiedAny; | ||
} | ||
interface SpyMap { | ||
[key: string]: jasmine.Spy; | ||
export declare type Spied<T> = { | ||
[K in keyof T]: SpiedMember; | ||
} | ||
interface ValueMap { | ||
[key: string]: any; | ||
export interface SpiedAny { | ||
[id: string]: SpiedMember | ||
} | ||
class DynamicMockBase<T extends object> { | ||
private spyMap: SpyMap = Object.create(null); | ||
private valueMap: ValueMap = Object.create(null); | ||
public handler = { | ||
export interface SpiedMember { | ||
_func?: jasmine.Spy; | ||
_get?: jasmine.Spy; | ||
_set?: jasmine.Spy; | ||
} | ||
interface Type<T> extends Function { | ||
new (...args: any[]): T; | ||
} | ||
class DynamicBase<T extends object> { | ||
public stubProxy: Mock<T>; | ||
private stub = Object.create(null); | ||
private spyProxy: T; | ||
private spy = Object.create(null); | ||
// create a spy before it is directly read/written | ||
private stubProxyHandler = { | ||
get: (target: T, propertyName: keyof T, receiver) => { | ||
// trying to get a property, return value from valueMap. | ||
if (typeof this.prototype[propertyName] !== 'function') { | ||
return this.valueMap[propertyName]; | ||
} | ||
if (propertyName === '_spy') { | ||
return this.spyProxy; | ||
} | ||
// trying to get a function, if we haven't created the spy, create one | ||
if (!this.spyMap[propertyName]) { | ||
const spy = jasmine.createSpy(propertyName); | ||
this.spyMap[propertyName] = spy; | ||
} | ||
this.ensureSpy(propertyName); | ||
return this.spyMap[propertyName]; | ||
return this.stub[propertyName]; | ||
}, | ||
// store whatever user wants in the value map | ||
set: (target, propertyName: keyof T, value, receiver) => { | ||
if (propertyName === '_spy') { | ||
throw Error('Cannot modify _spy. It is part of the MockFactory'); | ||
} | ||
if (typeof this.prototype[propertyName] === 'function') { | ||
throw Error(`Assignment not allowed because ${propertyName} is already a spied function`); | ||
throw Error(`Cannot change ${propertyName} function, because MockFactory has already attached a permanent spy to it`) | ||
} | ||
this.valueMap[propertyName] = value; | ||
this.ensureSpy(propertyName); | ||
this.stub[propertyName] = value; | ||
return true; | ||
@@ -42,3 +59,71 @@ }, | ||
constructor(private prototype: T) {} | ||
// create a spy before it is read from the spyFacade | ||
private spyProxyHanlder = { | ||
get: (target: T, propertyName: keyof T, receiver) => { | ||
this.ensureSpy(propertyName); | ||
return this.spy[propertyName]; | ||
}, | ||
set: (target, propertyName: keyof T, value, receiver) => { | ||
throw Error(`Cannot change _spy.${propertyName}, because it is part of the MockFactory`); | ||
}, | ||
} | ||
constructor(private prototype: T) { | ||
this.stubProxy = new Proxy<Mock<T>>(Object.create(null) as any as Mock<T>, this.stubProxyHandler); | ||
this.spyProxy = new Proxy(Object.create(null), this.spyProxyHanlder); | ||
} | ||
private ensureSpy(propertyName: keyof T): void { | ||
// create spy if needed | ||
if (!this.spy[propertyName]) { | ||
// if target is property | ||
if (typeof this.prototype[propertyName] !== 'function') { | ||
// we add getters and setters to all properties to make the read and write spy-able | ||
const descriptor = { | ||
get: /* istanbul ignore next: Can't reach. spyOnProperty() requires its presence to install spies */ () => {}, | ||
set: /* istanbul ignore next: Can't reach. spyOnProperty() requires its presence to install spies */ (value) => {}, | ||
enumerable: true, | ||
configurable: true, // required by spyOnProperty | ||
}; | ||
Object.defineProperty(this.stub, propertyName, descriptor); | ||
// by default, let getter spy return whatever setter spy receives | ||
const getterSpy = spyOnProperty(this.stub, propertyName, 'get').and.callFake(() => this.spy[propertyName]._value); | ||
const setterSpy = spyOnProperty(this.stub, propertyName, 'set').and.callFake(value => this.spy[propertyName]._value = value); | ||
this.spy[propertyName] = { | ||
_value: undefined, // this is not on the public API, because _value will become meaningless once user customizes the spies. | ||
_get: getterSpy, | ||
_set: setterSpy, | ||
} | ||
Object.defineProperty(this.spy[propertyName], '_func', { | ||
get: () => { throw Error(`can't get ${propertyName}._func because ${propertyName} is a property. You can config getter/setter spies via ${propertyName}._get and ${propertyName}._set`); }, | ||
set: () => { throw Error(`can't set ${propertyName}._func because ${propertyName} is a property. You can config getter/setter spies via ${propertyName}._get and ${propertyName}._set`); } | ||
}); | ||
// if target is function | ||
} else { | ||
const spy = jasmine.createSpy(propertyName); | ||
this.stub[propertyName] = spy; | ||
this.spy[propertyName] = { | ||
_func: spy, | ||
_get: undefined, | ||
_set: undefined, | ||
}; | ||
Object.defineProperty(this.spy[propertyName], '_get', { | ||
get: () => { throw Error(`can't get ${propertyName}._get because ${propertyName} is a function. You can config function spy via ${propertyName}._func`); }, | ||
set: () => { throw Error(`can't set ${propertyName}._get because ${propertyName} is a function. You can config function spy via ${propertyName}._func`); } | ||
}); | ||
Object.defineProperty(this.spy[propertyName], '_set', { | ||
get: () => { throw Error(`can't get ${propertyName}._set because ${propertyName} is a function. You can config function spy via ${propertyName}._func`); }, | ||
set: () => { throw Error(`can't set ${propertyName}._set because ${propertyName} is a function. You can config function spy via ${propertyName}._func`); } | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
@@ -48,7 +133,8 @@ | ||
/** | ||
* create a mock object that has the identical interface with the class you passed in | ||
* create a mock object that has the identical interface as the class you passed in | ||
*/ | ||
public static create<T extends object>(blueprint: Type<T> | T) { | ||
public static create<T extends object>(blueprint: Type<T> | T): Mock<T> { | ||
let prototype: T; | ||
if (blueprint['prototype']) { | ||
// get the prototype for a TypeScript class | ||
prototype = blueprint['prototype']; | ||
@@ -59,5 +145,5 @@ } else { | ||
const mockBase = new DynamicMockBase(prototype); | ||
return new Proxy<T>(mockBase as any as T, mockBase.handler); | ||
const dynamicBase = new DynamicBase(prototype); | ||
return dynamicBase.stubProxy; | ||
} | ||
} |
16659
6
121
188