-
-
Notifications
You must be signed in to change notification settings - Fork 770
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
ES6 classes and stub #831
Comments
We are trying to keep the GitHub issues list tidy and focused on bugs and feature discussions. This ticket looks like a usage question, please post it to the Sinon.JS mailinglist, so the bigger community can help answer your questions. If you feel that your topic is an issue with Sinon.JS, please open a new ticket and follow the guidelines for reporting an issue. |
@Spy-Seth Try this:
Then you could test it like this:
(using chai and sinon-chai) |
I was hoping that sinon could manage this correctly. It's conform me in the fact ES6 classes are not completely ready to be widely used: they can't be tested easily. |
Yes, this is weird... 🙀 |
There is nothing weird or different about ES6 classes. ES classes are sugar coated ES5 classes and can be stubbed in the same way using |
ping @Spy-Seth we can close this one no? @fatso83 yes in fact with the right babel config & release it's ok for us. |
I'm not sure if I should file this as a separate issue (since it's a different error than what @Spy-Seth reported), or if it should be part of this issue (since it's another problem with stubbing ES6 class constructors ... so I guess I'll post it here. When I stub a class's constructor with Sinon 1.17.3 it works ... but the stub never gets called. In other words if you do something like ...
you will see that the constructor stub was not called ... despite the Based on this quote from http://www.2ality.com/2015/02/es6-classes-final.html:
It appears that ES6 class constructors cannot be changed after construction, and so calling spy/stub/whatever on them is a nonsense operation. As such, I believe Sinon should throw an error when you try to stub a constructor, rather than letting the user believe that the stub worked. P.S. All of this relies on a "real" ES6 environment (eg. nodejs run with the --harmony flag), not Babel ES6=>ES5 code. |
@machineghost: user error. That's not how javascript works. Look into what MDN has to say on the constructor property. As stated above: nothing magic about ES6 classes. Just sugar coated syntax for ES5. Test them as such. If you want to see if a function gets called, just supply a stub instead. You want to check on Foo - not Foo.constructor. |
EDIT @fatso83 Did you try the example code I provided? Here's a simplified version of my example that doesn't use Sinon, which you can copy/paste directly in to the Chrome debugger to see in action:
I don't quite follow; if I change the last line of the previous example code from:
to:
then |
That was perhaps a bit too brief (typed on a mobile), so I understand the confusion. I'll have another go: Summed up:
|
Thanks a bunch @fatso83 for that explanation, it really helped. It sounds like what I'd like to do is impossible, but for reasons that have nothing to do with ES6. What I'm trying to accomplish is the following
While that sort of pattern works great with say a Backbone class (which has a separate, non-constructor Of course, I could get all dependency injection-y and make B's constructor take A as an argument so that I can test it ... but I think instead I'm just going to give up on doing this sort of assertion. But again, thanks a bunch for taking the time to provide that explanation. |
It is possible to achieve what you want, at least in Node, using heavier machinery like proxiquire, but I find that unnecessary. Just opening up a little bit for dependency injection is a pragmatic middle-way that does less magic IMHO, at the expense of a few lines. Doing it like you just said is something I sometimes do, as in this codebit I had lying around from a project a few years ago: /**
* Request proxy to intercept and cache outgoing http requests
*
* @param {Number} opts.maxAgeInSeconds how long a cached response should be valid before being refreshed
* @param {Number} opts.maxStaleInSeconds how long we are willing to use a stale cache in case of failing service requests
* @param {boolean} opts.useInMemCache default is false
* @param {Object} opts.stubs for dependency injection in unit tests
* @constructor
*/
function RequestCacher (opts) {
opts = opts || {};
this.maxAge = opts.maxAgeInSeconds || 60 * 60;
this.maxStale = opts.maxStaleInSeconds || 0;
this.useInMemCache = !!opts.useInMemCache;
this.useBasicToken = !!opts.useBasicToken;
this.useBearerToken = !!opts.useBearerToken;
if (!opts.stubs) {
opts.stubs = {};
}
this._redisCache = opts.stubs.redisCache || require('./redis-cache');
this._externalRequest = opts.stubs.externalRequest || require('../request-helpers/external-request-handler');
this._memCache = opts.stubs.memCache || SimpleMemCache.getSharedInstance();
}
RequestCacher.prototype = { /* methods ... */ }
module.exports = RequestCacher By using an options argument, instead of relying on the positional arguments, it keeps flexible, but often I just export some factory methods to produce objects in various ways instead of, or (for convenience) in addition to, directly exposing the constructor. For instance, taking your own example of module.exports = class B {
constructor() {
this.a = new A();
}
doSomething() { return a.foo(); }
};
// alternative factory method
B.createUsing = (injectedDependency) => {
const b = Object.create(B.prototype); // will not invoke the constructor!
b.a = injectedDependency;
}
// test that B is using A correctly
const aStub = { foo : sinon.stub() };
const b = B.createUsing(aStub);
b.doSomething();
assert.true(a.foo.calledOnce); No extra magic, but a few lines extra. Granted, it still doesn't test the actual constructor, but I usually try to leave testing to code lines with a higher ROI 😏 |
Hmmm ... well your example makes a very good case for (minimal) dependency injection, and I'm sure there's something to the idea since the Angular team embraces it so heavily. But at the same time, I have a very strong (I'd argue "healthy") aversion to changing production code purely to support test code. So, I guess I just have to weigh my distaste for, on the one hand, making test-only changes to non-test code vs. my desire to test whether |
Thanks, so I came here in search of a solution to the OP's problem, which seems to be similar to the issue I'm having. I guess this won't be a huge issue until more people start to use ES6 native classes. Since this issue is closed, I'm assuming we have to seek an alternative? Any new thoughts on this @machineghost? |
@fatso83, yes I do believe you are correct. However, I've used proxyquire, and it does not let you stub out multiple instances of a class. So for instance if I have this // carousel.js
import Panel from 'panel';
class Carousel {
/**
* When the carousel is instantiated.
* @param {HTMLCollection|NodeList} panels - The elements that will represent the panels
*/
constructor (panels) {
this._panelInstances = {};
for (var i=0; i < panels.length; i++) {
this._panelInstances['panel' + i] = new Panel(panels[i]);
}
}
} Even with proxyquire, there is no way to stub the Panel's constructor to test that Ideally, I would want to do something like this... // carousel-test.js
import Panel from 'panel';
import Carousel from 'carousel';
var firstPanelInstance = sinon.createStubInstance(Panel);
var carouselPanelConstructor = sinon.stub(Carousel.prototype, 'constructor');
var panelEl = document.getElementById("panel1");
var firstPanelInstance = carouselPanelConstructor.withArgs(panelEl).returns(firstPanelInstance);
var carousel = new Carousel([panelEl]);
assert.equal(firstPanelInstance.args[0][0], panelEl, 'first panel instance was created correctly');
// ... |
@mkay581 You are missing something. I have now made this example to show an example of
I am going to lock the discussion on this one from now on. For latecomers these are the main points to take to heart:
|
I'm working on an ES6 project, and I'm trying to stub ES6 classes. How can I do this?
My class:
And when I tried to test it:
Is there a way to stub ES6 classes?
The text was updated successfully, but these errors were encountered: