Notable changes
Notable changes from https://sinonjs.org/guides/migration-guide.html, based on our (limited) usage:
Sinon 2: sinon.useFakeXMLHttpRequest
This should be replaced with sandbox.useFakeServer (both exist in Sinon 1).
- Difficulty: Easy
We already use useFakeServer() everywhere. Except one test in the UploadWizard extension (and a copy of that code, in MediaUploader) which oddly and redundantly calls both.
Sinon 2: Replace stub(obj, method, fn) with stub(ob, method).callsFake(fn)
- Difficulty: Medium.
Trivial find-and-replace, but in a lot of places.
The bad news is that the replacement, callsFake, is introduced in the same version which means we can't migrate ahead of time. The good news is that while it emits warnings, the old way does still work in Sinon 2.x, so we can at least prepare for the bulk of our use by iterating in CI and then merging the long tail after the upgrade. The other bad news is that the old way is removed in Sinon 3.0, which means we have to do this in multiple stages. However, if this is the only hard milestone, we could patch the latest Sinon to temporarily monkey-patch this back in, so that we can do the migration in one step.
Sinon 3: Remove support for sandbox.stub(obj, 'nonExistingProperty')
- Difficulty: TBD.
I vaguely recall using this in a few places to stub non-existent browser APIs, as well as to workaround the fact that Sinon doesn't permit stubbing of non-function values. This might not be an issue anymore, however.
Sinon 3: Replace sinon.useFakeTimers(now, ...prop) with sinon.useFakeTimers( { now, toFake } )
- Difficulty: Medium.
The good news is that the vast majority of our usage is simply sinon.useFakeTimers() or sinon.useFakeTimers(now), which are unchanged.
The bad news is that this is yet another removal without deprecation, where the replacement is new in the same release, which means we can't prepare/migrate ahead of time. Where previously you'd call sinon.useFakeTimers(now, 'Date', 'setInterval') one must now call sinon.useFakeTimers({ now, toFake: ['Date'] }), where toFake is an array of date-related properties to globally.
The feature being deprecated here is imho an anti-pattern. The prop here refers to faking only a subset of date-related browser features. E.g. faking new Date but not setTimeout. I suspect a number of callers in our codebase are probably didn't know that this was an optional argument, or thought it was better to specify what you need, rather than an actual requirement or dependency to only part of the global clock progression. In any event, this will definitely need a monkey-patch since it skipped deprecation (unless there is e.g. only 1 caller and we force-merge that if CI passes in core with that patch applies).
Full list:
https://sinonjs.org/releases/v18/fake-timers/
https://github.com/sinonjs/fake-timers/tree/v13.0.5?tab=readme-ov-file#api-reference
["setTimeout", "clearTimeout", "setImmediate", "clearImmediate", "setInterval", "clearInterval", "Date", "requestAnimationFrame", "cancelAnimationFrame", "requestIdleCallback", "cancelIdleCallback", "hrtime", "performance"]
Sinon 4: (none)
Sinon 5: The sinon global is now itself a restorable sandbox.
This is not a breaking change, but a new feature, and quite a nice one.
Up until now, the main Sinon API was stateless so while you could call sinon.stub() any time and restore these individually, the would not be tracked anywhere as part of a group that you can verify and restore wholesale.
Instead, to automatically restore all stubs (as MediaWiki does after each test), we create and assign a sandbox group this.sandbox = sinon.sandbox.create();, followed by your test calling this.sandbox.stub(), and then eventually MediaWIki calls this.sandbox.restore().
After this upgrade,, we can use sinon.stub() directly in any QUnit test. MediaWiki will use the new global sinon.restore() after each test. Sinon still supports ad-hoc sandboxes all the same, this is an additional way to use it, and a much more ergonomic one.
QUnit.test('example', function (assert) { var x = this.sandbox.stub(thing, 'hello'); // … });
QUnit.test('example', function (assert) { var x = sinon.stub(thing, 'hello'); // … x.restore(); });
QUnit.test('example', (assert) => { var x = sinon.stub(thing, 'hello'); // … });
Sinon 4: spy.reset() removed in favour of spy.resetHistory()
- Difficulty: Medium.
Doesn't impact us much since we generally use sinon.stub() and not sinon.spy(). And even then while you may in rare cases want to .restore() a stub or spy mid-way a test, it is even more rare to do a partial restore by calling .reset().
Spies are a low-level concept in Sinon. Every stub uses a spy underneath. To reset a spy means to reset its call counters and argument history, which used to be spy.reset() and is now called spy.resetHistory(). To reset a stub means to reset its spy history and to reset any returnsValue behaviour overrides, which is unchanged and continues to be called stub.reset() although the second part of that is now also exposed as stub.resetBehaviour so stub.reset() is a shortcut for stub.resetHistory and stub.resetBehaviour. You can see how this is much cleaner, given that stub inherits spy, when these methods have their own name of this.
When using Codesearch to look for .reset() you're likely find cases that reset a stub, not a spy. So it's hard to find, but fortunately there aren't many.
Sinon 6 - Sinon 18: (none relevant)
Misc - Creating and replacing non-function properties
I haven't tried to narrow down when this unofficial feature changed, but it used to be possible to create or replace arbitrary properties such as plain objects, or even entire classes. Given that later Sinon versions specialise more into the function behaviors, this stopped working at some point, e.g. given var x = { a :1 }; something like sinon.stub( x, 'a', 2 ); is not valid.
The equivalent for this, is sinon.define(obj, prop, val) and sinon.replace(obj, prop, val).
Proposal
Plan A
- Prep for Sinon 2 and Sinon 5:
- Remove use of sinon.useFakeXMLHttpRequest.
- Migrate handful of calls to sinon.reset().
- Upgrade from Sinon 1.x to Sinon 18, with a monkey-patch that restores stub(obj, method, fn), spy.reset(), and useFakeTimers(now, prop).
- Migrate stub(obj, method, fn) and useFakeTimers(now, prop).
Sentiment: Majority of code can migrate at ease after the upgrade, no breakage or disruption.
Plan B
- Prep for Sinon 2: Remove use of sinon.useFakeXMLHttpRequest.
- Upgrade from Sinon 1.x to Sinon 2
- Migrate stub(obj, method, fn) to stub(ob, method).callsFake(fn).
- Upgrade from Sinon 2 to Sinon 3, with a monkey-patch that restores useFakeTimers(now, prop)
- Migrate useFakeTimers(now, prop).
- Backport sinon.resetHistory atop Sinon 1.x, and migrate handful of calls to sinon.reset().
- Upgrade from Sinon 3 to Sinon 18.
Sentiment: At three distinct points, tests in repos not updated in time will break.