Integrations: Sinon with QUnit
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 4k
- Forks
- 773
- PR merge metrics
- No merged PRs in 30d
Description
Another one for the "Integrations" section (ref https://github.com/qunitjs/qunitjs.com/issues/42), we can promote use of SinonJS.
Perhaps a very plain and simple way, could be like so:
const { test } = QUnit;
const sandbox = require('sinon').sandbox.create();
QUnit.module('example', hooks => {
hooks.afterEach(() => {
sandbox.restore();
});
test('sinon stub', assert => {
const spy = sandbox.stub(console, 'log');
console.log('foo');
assert.true(spy.calledOnce);
assert.true(spy.calledWith('foo'));
});
});
With Sinon 5 and later, the sandox creation is optional per https://sinonjs.org/guides/migrating-to-5.0 so it could even simpler:
const { test } = QUnit;
const sinon = require('sinon');
QUnit.module('example', hooks => {
hooks.afterEach(() => {
sinon.restore();
});
test('sinon stub', assert => {
const spy = sinon.stub(console, 'log');
console.log('foo');
assert.true(spy.calledOnce);
assert.true(spy.calledWith('foo'));
});
});
Based on anecdotal searches in the wild, the above seems to be how most people use Sinon with QUnit.
However, the above would not benefit from Sinon's descriptive error messages. That's why often people avoid Sinon's boolean shortcut methods like calledWith() in favour of performing native QUnit assertions on the spy object, like so:
// assert.true(spy.calledWith('foo'));
const call = spy.getCall(0);
assert.equal(call.args[0], 'foo');
But, this isn't a great developer experience and means most documentation and tutorials out there for Sinon won't feel applicable. Instead, we can use Sinon.assert and let it throw descriptive exception messages. This is what https://sinonjs.org/releases/v9.0.3/sandbox/ recommends:
const { test } = QUnit;
const sinon = require('sinon');
QUnit.module('example', hooks => {
hooks.afterEach(() => {
sinon.restore();
});
test('sinon stub', assert => {
const spy = sinon.stub(console, 'log');
console.log('foo');
sinon.assert.calledOnce(spy);
sinon.assert.calledWith('foo');
});
});
This seems much better for the developer, but it is by default optimises for exception-based test frameworks like Mocha and JUnit, which stop after the first error. QUnit won't see the exceptions as assertion failures so they would technically appear as unexpected exceptions. Which isn't so bad by itself as the UI for that is mostly the same, but it might also trigger "zero assertions" warnings if the test has no other assertions, and that's kind of a deal breaker imho.
Sinon provides an extensible assert API for that reason, so that you can really easily hook it up to QUnit. Per https://sinonjs.org/releases/v9.0.3/assertions/:
sinon.assert.pass = function (assertion /* string */ ) {
// …
};
sinon.assert.fail = function (message /* string */ ) {
// …
};
The thing is, these are globally static and not part if Sinon's restorable sandbox model. Which means even with before/after hooks, or Sinon's sandbox.injectInto feature, it would not be easy to connect it to a local assertion object. (Aside from using global QUnit.config.current.assert.)
But, there is a dedicated sinon.assert.expose(obj); method that does what we need by providing the assertion methods with support for local pass and fail callbacks. Below is a hacky way to connect that, as demonstration:
hooks.beforeEach(assert => {
assert.sinon = {};
sinon.assert.expose(assert.sinon, { prefix: '' });
assert.sinon.pass = (message) => assert.pushResult({ result: true, expected: true, actual: true, message });
assert.sinon.fail = (message) => assert.pushResult({ result: false, expected: true, actual: false, message });
});
test('sinon stub', assert => {
const spy = sinon.stub(console, 'log');
console.log('foo');
assert.sinon.calledOnce(spy);
assert.sinon.calledWith('foo');
});
This seems to provide the best of both worlds, although the setup is obviously impractical, so we'd want to ship that in a plugin that handles this automatically. There are numerous sinon-qunit plugins out there we could work with, perhaps some of them even do this already?
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the Integrations section referenced in issue 42 and the linked Sinon sandbox and assertions guides. Compare existing sinon-qunit plugins and determine whether one provides the local assertion integration described here. Done means selecting and documenting a practical QUnit/Sinon integration, or identifying the plugin work required.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- documentation, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100