Question:
Sinon 2.2.0 introduces stub.usingPromise()
, which let's one use a custom Promise implementation on a per-stub basis. This is great, thanks for that! 🎉
My question is: what do you think about providing a global configuration option (e.g. per test file) for specifying a custom Promise implementation?
For example, similar to how sinon-as-promised
did it back in the old days: https://github.com/bendrucker/sinon-as-promised#usage
const Bluebird = require('bluebird');
require('sinon-as-promised')(Bluebird);
I am not opposed to making changes to accommodate per-instance configuration. The proposed changes will lead to a new MAJOR
version, as the API will change significantly, which is fine.
Here's what I am thinking: currently sinon
is an Object
. What you're proposing will lead to sinon
becoming a Function
that accepts an options Object
(let's not paint ourselves into a corner and expect the first argument to be a Promise implementation).
So something like this?
const Bluebird = require('bluebird');
const sinon = require('sinon')({ promiseImplementation: Bluebird});
What do you think? Ping @sinonjs/core
I think @mroderick's idea to change the Sinon object into a function is great. Having a place to pass configs into a Sinon instance will sure be useful in the future, not only for promise configuration.
However, I think this can be done in a backward compatible way by setting all the current Sinon object properties on the function. I would like to keep the current "stateless" nature and not force everybody to do const sinon = require('sinon')()
who wants to current behavior. We already do this with sinon.match
, which is a function with additional properties like sinon.match.string
etc.
Another option would be to expose a factory function on the sinon object; ie:
const mySinon = sinon.withConfig({ promiseImplementation: Bluebird });
However, I think this can be done in a backward compatible way by setting all the current Sinon object properties on the function. I would like to keep the current "stateless" nature and not force everybody to do const sinon = require('sinon')() who wants to current behavior. We already do this with sinon.match, which is a function with additional properties like sinon.match.string etc.
That was what I was thinking, but failed to express.
To follow SemVer, we will still need to publish a new MAJOR
version, as the module is not exporting an Object
, but a Function
. I do not see this as an obstacle.
I am slightly more in favour of changing the export to a factory Function
, rather than creating a factory method on the exported Object
.
I have no hard feelings on either solution. @mroderick I agree that changing the object to a function is a major release, but adding the factory function is a minor, right? Wanted to add a label 🏷
@mantoni that is my understanding too
Hi, friends, I was working on this a bit yesterday and I came up with a few moments of indecision and I'd like to explain my thoughts to you so you can approve what I'm doing before I submit a PR.
symbol
avoids namespace clashes and we could make this symbol available through the current defaultConfigs
so that we can get stuff from it whenever we need. However, I think this approach is not optimal because it opens up a great possibility of memory leaks (since garbage colletor will always keep global[sinonSymbol]
alive.require
currently works (it looks for an instance of the required file in cache and returns it if exists) we can simply get the same configs instance whenever we require sinonConfigs
. However, in order to implement this, I think the best option would be to use the existing getConfig
, but it is being listed as deprecated
when we exposeCoreUtils
.getConfig
(renaming it to withConfig
) we could simply overwrite the default globalPromise
prop and use it whenever we want because the defaultConfig
object would be cached and always be the same in the future.Also, do we plan on returning a new Sinon instance whenever we call withConfig
or do we want to return the same instance and just change its configs?
+1 for this feature. I'm running some issues trying to use bluebird + sinon 1 + sinon-as-promised latest versions with an error about:
TypeError: Cannot set property 'resolves' of undefined
>> at Object.<anonymous> (/home/lobo/dev/petrocloud/pc3/node_modules/sinon-as-promised/index.js:14:25)
That is because Sinon 2 has no resolves, as sinon 2 supports native promises.
Copying from pull request #1542 to keep everything in one place:
fatso83:
I just wanted to ensure I understand you correctly in what you want to achieve before letting this go: I assume you wanted to change the
sinon
instance in one test file and have it automatically propagate to the rest of your test code. Am I right? If so, I think this is exactly what we are trying to avoid. Testing is best done when the code is explicit, and configuring a sinon instance that will work as advertised for that one specific file is usually what we strive for.
As to simplicity, I don't see all the negative downsides of doing:
const sinon = require('sinon')({promiseImplementation: bluebird})
in each of the test files you need to change this. Am I mistaking what the problem is? I think I might, as I am not sure I got what you meant by "override the require cache". I think you meant that if you wanted to reuse that same instance in all of your different test files then you would need to hack the require cache, but that seems more trouble than it's worth compared to simply passing in a config object.
druotic:
@fatso83 Nope, you're correct. In our case, we use Bluebird promises exclusively (though, that'll change when async/await/node 8 goes LTS in October). In our previous setup before upgrading to sinon 3, we just required
sinon-as-promised
before running our tests, and all future stubs/mocks used bluebird promises. We wanted to maintain that behavior since we never want to accidentally use native promises.
We could explicitly pass in the promise implementation in every test file where we need it, but that would be 100% of test files in our case. If another dev forgets to pass it in, that could lead to broken behavior/bad assertions. It'd be nice to be able to just specify it once since we never want to use native promises.
edit: Maybe there could be a
setDefaultConfig
function that clearly indicates by the name that it has global impact and that people shouldn't use it on a per-test basis? It feels likesinon.addBehavior
could live there as well since it has global impact - e.g.customStubBehaviors: [ { 'returnsNum': (fake, n) => fake.returns(n) }, ... ]
@lucasfcosta I would really like to land this to avoid bumping the major version twice in short succession (due to #1557), so if you have code to support a configurable Sinon instance, I think a PR would be most welcome. As I saw there was a bit of confusion, I'll touch on most of your points below.
As to 1 (regarding state), yes, we would have to pass in a config object to the various functions in order to support different instances of sinon, and this requires a bit of change in the underlying modules. Not a lot, though, as the amount of config that can be changed today pretty much amounts to promises
, I think. So it would probably be a lot of passing the config into various places, but not necessarily making use of it everywhere.
However, in order to implement this, I think the best option would be to use the existing getConfig, but it is being listed as deprecated when we exposeCoreUtils.
getConfig
is not deprecated. It is only the _exported function_ that was deprecated from the _public API_. It is totally _fine_ to keep on reusing it internally. We just didn't want external libs to depend on its behavior.
Also, do we plan on returning a new Sinon instance whenever we call
withConfig
or do we want to return the same instance and just change its configs?
I think the clearest option here is to return a new instance for each config, but perhaps caching the default instance, as it will probably be used in most cases. It's up to the clients to cache the instance of they feel they need to (say, as an export in my-sinon.js
). As for performance, most of the time is probably used in system calls to the file system (via require
), so caching individual instances is probably a micro-optimisation.
Another thing, is that from the discussion above, I think we landed on making sinon
a factory, instead of adding a factory method, so withConfig
is out the door :smile_cat: As Phred sayd in the group discussed on Twitter:
require('sinon')
should have the same api and properties asrequire('sinon')(config)
.
P.S. We might consider moving the discussion to a separate issue, as this one deals with something entirely different, although dependent on the discussed change being present.
@mroderick Regarding the factory that would be attached to the sinon
object to get a configured instance instead of the currently available object, I think it should be done similarly to the sandbox naming.
So I just would use
const mySinon = sinon.create(config);
This is much more intuitive than introducing a different factory function name here.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Most helpful comment
Another option would be to expose a factory function on the sinon object; ie: