Implement spies for get/set functions on accessor properties

This commit is contained in:
Sukharev Maxim
2015-12-30 13:01:19 +07:00
committed by Joe Cellucci
parent 8624a52ee0
commit 56191cfb2e
4 changed files with 258 additions and 0 deletions

View File

@@ -77,6 +77,126 @@ describe("SpyRegistry", function() {
});
});
describe("#spyOnProperty", function() {
// IE 8 doesn't support `definePropery` on non-DOM nodes
if (jasmine.getEnv().ieVersion < 9) { return; }
it("checks for the existence of the object", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry();
expect(function() {
spyRegistry.spyOnProperty(void 0, 'pants');
}).toThrowError(/could not find an object/);
});
it("checks that a property name was passed", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {};
expect(function() {
spyRegistry.spyOnProperty(subject);
}).toThrowError(/No property name supplied/);
});
it("checks for the existence of the method", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {};
expect(function() {
spyRegistry.spyOnProperty(subject, 'pants');
}).toThrowError(/property does not exist/);
});
it("checks for the existence of access type", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {};
Object.defineProperty(subject, 'pants', {
get: function() { return 1; },
configurable: true
});
expect(function() {
spyRegistry.spyOnProperty(subject, 'pants', 'set');
}).toThrowError(/does not have access type/);
});
it("checks if it has already been spied upon", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {};
Object.defineProperty(subject, 'spiedProp', {
get: function() { return 1; },
configurable: true
});
spyRegistry.spyOnProperty(subject, 'spiedProp');
expect(function() {
spyRegistry.spyOnProperty(subject, 'spiedProp');
}).toThrowError(/has already been spied upon/);
});
it("checks if it can be spied upon", function() {
var subject = {};
Object.defineProperty(subject, 'myProp', {
get: function() {}
});
Object.defineProperty(subject, 'spiedProp', {
get: function() {},
configurable: true
});
var spyRegistry = new jasmineUnderTest.SpyRegistry();
expect(function() {
spyRegistry.spyOnProperty(subject, 'myProp');
}).toThrowError(/is not declared configurable/);
expect(function() {
spyRegistry.spyOnProperty(subject, 'spiedProp');
}).not.toThrowError(/is not declared configurable/);
});
it("overrides the property getter on the object and returns the spy", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {},
returnValue = 1;
Object.defineProperty(subject, 'spiedProperty', {
get: function() { return returnValue; },
configurable: true
});
expect(subject.spiedProperty).toEqual(returnValue);
var spy = spyRegistry.spyOnProperty(subject, 'spiedProperty');
var getter = Object.getOwnPropertyDescriptor(subject, 'spiedProperty').get;
expect(getter).toEqual(spy);
expect(subject.spiedProperty).toBeUndefined();
});
it("overrides the property setter on the object and returns the spy", function() {
var spyRegistry = new jasmineUnderTest.SpyRegistry(),
subject = {},
returnValue = 1;
Object.defineProperty(subject, 'spiedProperty', {
get: function() { return returnValue; },
set: function() {},
configurable: true
});
var spy = spyRegistry.spyOnProperty(subject, 'spiedProperty', 'set');
var setter = Object.getOwnPropertyDescriptor(subject, 'spiedProperty').set;
expect(subject.spiedProperty).toEqual(returnValue);
expect(setter).toEqual(spy);
});
});
describe("#clearSpies", function() {
it("restores the original functions on the spied-upon objects", function() {
var spies = [],
@@ -152,4 +272,51 @@ describe("SpyRegistry", function() {
expect(jasmineUnderTest.isSpy(subject.spiedFunc)).toBe(false);
});
});
describe('spying on properties', function() {
it("restores the original properties on the spied-upon objects", function() {
// IE 8 doesn't support `definePropery` on non-DOM nodes
if (jasmine.getEnv().ieVersion < 9) { return; }
var spies = [],
spyRegistry = new jasmineUnderTest.SpyRegistry({currentSpies: function() { return spies; }}),
originalReturn = 1,
subject = {};
Object.defineProperty(subject, 'spiedProp', {
get: function() { return originalReturn; },
configurable: true
});
spyRegistry.spyOnProperty(subject, 'spiedProp');
spyRegistry.clearSpies();
expect(subject.spiedProp).toBe(originalReturn);
});
it("does not add a property that the spied-upon object didn't originally have", function() {
// IE 8 doesn't support `Object.create`
if (jasmine.getEnv().ieVersion < 9) { return; }
var spies = [],
spyRegistry = new jasmineUnderTest.SpyRegistry({currentSpies: function() { return spies; }}),
originalReturn = 1,
subjectParent = {};
Object.defineProperty(subjectParent, 'spiedProp', {
get: function() { return originalReturn; },
configurable: true
});
var subject = Object.create(subjectParent);
expect(subject.hasOwnProperty('spiedProp')).toBe(false);
spyRegistry.spyOnProperty(subject, 'spiedProp');
spyRegistry.clearSpies();
expect(subject.hasOwnProperty('spiedProp')).toBe(false);
expect(subject.spiedProp).toBe(originalReturn);
});
});
});

View File

@@ -25,4 +25,23 @@ describe("jasmineUnderTest.util", function() {
expect(jasmineUnderTest.util.isUndefined(undefined)).toBe(false);
});
});
describe("getPropertyDescriptor", function() {
it("get property descriptor from object", function() {
var obj = {prop: 1},
actual = jasmineUnderTest.util.getPropertyDescriptor(obj, 'prop'),
expected = Object.getOwnPropertyDescriptor(obj, 'prop');
expect(actual).toEqual(expected);
});
it("get property descriptor from object property", function() {
var proto = {prop: 1},
obj = Object.create(proto),
actual = jasmineUnderTest.util.getPropertyDescriptor(proto, 'prop'),
expected = Object.getOwnPropertyDescriptor(proto, 'prop');
expect(actual).toEqual(expected);
});
});
});

View File

@@ -68,6 +68,66 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
return spiedMethod;
};
this.spyOnProperty = function (obj, propertyName, accessType) {
accessType = accessType || 'get';
if (j$.util.isUndefined(obj)) {
throw new Error('spyOn could not find an object to spy upon for ' + propertyName + '');
}
if (j$.util.isUndefined(propertyName)) {
throw new Error('No property name supplied');
}
var descriptor;
try {
descriptor = j$.util.getPropertyDescriptor(obj, propertyName);
} catch(e) {
// IE 8 doesn't support `definePropery` on non-DOM nodes
}
if (!descriptor) {
throw new Error(propertyName + ' property does not exist');
}
if (!descriptor.configurable) {
throw new Error(propertyName + ' is not declared configurable');
}
if(!descriptor[accessType]) {
throw new Error('Property ' + propertyName + ' does not have access type ' + accessType);
}
if (j$.isSpy(descriptor[accessType])) {
//TODO?: should this return the current spy? Downside: may cause user confusion about spy state
throw new Error(propertyName + ' has already been spied upon');
}
var originalDescriptor = j$.util.clone(descriptor),
spy = j$.createSpy(propertyName, descriptor[accessType]),
restoreStrategy;
if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {
restoreStrategy = function() {
Object.defineProperty(obj, propertyName, originalDescriptor);
};
} else {
restoreStrategy = function() {
delete obj[propertyName];
};
}
currentSpies().push({
restoreObjectToOriginalState: restoreStrategy
});
descriptor[accessType] = spy;
Object.defineProperty(obj, propertyName, descriptor);
return spy;
};
this.clearSpies = function() {
var spies = currentSpies();
for (var i = spies.length - 1; i >= 0; i--) {

View File

@@ -55,5 +55,17 @@ getJasmineRequireObj().util = function() {
return cloned;
};
util.getPropertyDescriptor = function(obj, methodName) {
var descriptor,
proto = obj;
do {
descriptor = Object.getOwnPropertyDescriptor(proto, methodName);
proto = Object.getPrototypeOf(proto);
} while (!descriptor && proto);
return descriptor;
};
return util;
};