-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Devise a way to track & restore spies #38
Comments
Alternatively, a simpler approach would be to provide a spy.on(foo, 'bar');
foo.bar('Spy me!');
foo.bar.restore(); // restores original foo.bar() |
@epsitec we certainly could do that. I did think about, but I kind of don't like it for 2 reasons:
Maybe though... maybe individual sandbox = chai.spy.sandbox();
sandbox.spy.on(Array, 'push', 'pop');
sandbox.restore(); // restores Array push AND pop, is the only non-spy method on sandbox. |
@ketihamus you have convinced me. I am looking forward to test the sandboxes on chai.spy. |
😄 |
@keithamus your points are strong. Using sandbox over individual Didn't catch the idea of decorator. |
The point of the decorator is that you pass in a callback function which automatically restores spies after execution of the function - saving you all of the boilerplate, take the following two code examples: it('pushes items to array', ()=> {
let spy = chai.spy.sandbox();
spy.on(Array, 'push');
// some tests...
spy.restore();
}); it('pushes items to array', chai.spy.sandbox((spy)=> {
spy.on(Array, 'push');
// some tests...
})); @wsb9 do you think it's really necessary to be able to restore single spies, or would you typically restore a bunch/all of them? I think just having one method |
To extend a bit more on the whole restore one vs restore all - I think if you have the ability to restore one at a time, you can end up in a bit of a mess - if I spy on 5 methods and only restore 2, when do the remaining 3 get cleaned up? Or do they just sit there waiting for someone to trip up on them later down the line. I guess conversely if I want to restore 2 methods but keep spying on the other 3, then having to re-spy on the other 3 could be a pain. Not sure what's best here. |
@keithamus i think there may be situations where individual restore would be handy, but can't find where right now :) I'd just clean up whole sandbox in test runner hook after test case (mocha's When you need to leave half of spies while restoring other half, then spies possibly belong to different pieces of logic and it worth to factor them out to two different explicit sandboxes. And release on appropriate time. |
I think you've pretty much nailed it @wsb9. I'd say we're ready enough with this design to try to get it working. To summarise:
If anyone wants to take up the challenge of making a PR, I'd definitely look forward to seeing it. Otherwise I might work on this in a week or two. |
@keithamus this is great idea! The only issue which I currently see is
chai.spy.on(localStorage, 'get').returns('test')
var cache = {};
chai.spy.on(localStorage, 'get').as((key) => cache[key]);
// under the hood
on(object, methodName) {
this._sandbox.addForRestore(object, methodName, object[methodName]);
object[methodName] = spy();
return { // TODO: move to constructor with methods in prototype?
returns: function(value) {
object[methodName] = spy.returns(value)
},
as: function(fn) {
object[methodName] = spy(fn)
}
};
all(object, ..methods) {
methods.forEach((methodName) => {
this._sandbox.addForRestore(object, methodName, object[methodName]);
object[methodName] = spy();
});
return object;
} |
@stalniy what if we have // localStoage.getItem is spied on
chai.spy.on(localStorage, 'getItem');
// localStorage.getItem is spied on, and its behaviour is overriden
var cache = {};
chai.spy.on(localStorage, 'getItem', (key) => cache[key]);
// localStorage.getItem is spied on, and always returns `'test'`
chai.spy.on(localStorage, 'getItem', 'test');
// spy on 'getItem' and 'setItem'
chai.spy.on.all(localStorage, 'getItem', 'setItem');
// spy on all enumerable ('getItem', 'setItem', 'removeItem', 'clear')
chai.spy.on.all(localStorage); |
@keithamus Totally agree about However this is a good alternative and may work as the first api version (it won't be hard to implement better backward compatible solution in future) |
@keithamus probably I found a good compromise for using const push = spy.on(array, 'push').returns(5)
push.returns(6) // throws exception "spy returns is not a function" I'd like spy to still be immutable, so What do you think? |
@stalniy just spitballing here, but what about, instead, doing something like |
@keithamus sorry, but I think my suggestion is more clear :) Basically I suggest to split functionality into 2 types of spies (both are immutable):
So, // regular spy
const method = chai.spy()
console.log(typeof method.returns) // undefined
// tracking spy
const method = chai.spy.on(object, 'method')
expect(method).to.be.spy // true
object.method === method // true
const methodReturns5 = method.returns(5)e
expect(methodReturns5).to.be.spy // true
methodReturns5 === method // false
object.method === methodReturns5 // true
method.returns(6) // throw spy is already configured/locked/etc |
My problem with that API is that it is not immutable by design - only in the checks we supply; it is an easy way to trip someone up who isn't aware of our design decisions ("why can I do Secondly, and I think maybe more importantly, is that we're changing the surface of the API that the method has, e.g.: Array.prototype.push.returns // undefined
[].push.returns // undefined
chai.spy.on(Array.prototype, 'push');
Array.prototype.push.returns // function
[].push.returns // function If my experience with chai says anything, is that this will cause a bunch of weird permutations to happen in various libraries - or at the very least cause problems for anyone who wants to spy on a method which has a IMO the best solution is: // regular spy
const method = chai.spy()
// tracking spy
const method = chai.spy.on(object, 'method')
expect(method).to.be.spy
object.method === method
chai.spy.restore(object, 'method');
object.method !== method
const methodReturns5 = chai.spy.on(object, 'method', () => 5);
methodReturns5 !== method
object.method === methodReturns5
// Or, if you want a shorthand:
chai.spy.restore(object, 'method');
const methodReturns6 = chai.spy.on(object, 'method', chai.spy.returns(6)); |
First of all, I really like About function me(){}
me.test = true
var spy = chai.spy(me)
spy.test // undefined And there is nothing we can do with this because properties may be defined as non-enumerable or using |
Actually it's immutable in relation to spy object because As alternative api, we can return const spyBuilder = spy.on(array, 'push')
const original = spyBuilder.calls('original') // creates new spy and invokes original method when spy is called
const fake = spyBuilder.calls((...args) => args[0]) // creates new spy and calls passed function
const fakeReturn = spyBuilder.returns(5) // creates new spy which returns 5, eventually when array.push is called fakeReturn will be called and others - not But again this approach uses side effect... What is actually the main problem in such solution. That's why I suggested to allow to do this only once. Also this could be noted in docs and put So, const { returns } = chai.spy
chai.spy.on(array, 'push', returns(5)) is better and even more or less good in terms of readability but it restricts the whole DSL syntax to passing stuff as parameters. But first of all I will work on sandboxes and then will get back to this. |
Also adds DEFAULT_SANDBOX and all spies from chai.spy.on and chai.spy.object are tracked under DEFAULT_SANDBOX Fixes chaijs#38
Also adds DEFAULT_SANDBOX and all spies from chai.spy.on are tracked under DEFAULT_SANDBOX Fixes chaijs#38
Other alternatives for
|
One big problem with spies is being able to quickly restore old behaviour, when you want the function to execute its original behaviours.
Now we have
chai.spy.on(object, ...methods);
I think we could track which methods we spy on what objects. We could implement some kind of sandboxing:The main export of chai-spies could also be a sandbox in of itself - and so:
Potentially we could also make a decorator/higher order function which would help inside of tests; for example:
This is just a rough draft of what I have in my head - I'd very much like feedback, especially from @stalniy who has been doing some great contributions lately, including the improved
chai.spy.on
interface.The text was updated successfully, but these errors were encountered: