Why do these sinon stubs resolve to undefined?

Tags: , , , ,



I’ve written a unit test for the following code and stubbed the browser methods (read: web-extension API) using Sinon (more specifically: sinon-chrome, a dated but still functioning library for my use case).

/**
 * Returns an array of languages based on getAcceptLanguages and getUILanguage to use as defaults
 * for when no saved languages exist in browser storage.
 *
 * @memberof Helpers
 * @returns {array} Array of language codes i.e. ['en-US', 'fr']
 */
async function getDefaultLanguages () {
  const acceptedLanguages = await browser.i18n.getAcceptLanguages()
  const uiLanguage = browser.i18n.getUILanguage()

  return [uiLanguage].concat(acceptedLanguages)
}

The unit test:

const sinon = require('sinon')
const browser = require('sinon-chrome/extensions')
const { assert } = require('chai')
const helpers = require('../src/helpers')

// helpers that rely on the web-extension API (will need to be mocked)
describe('Helpers: Web-Extension API', function () {
  const { getDefaultLanguages } = helpers

  let languages

  before(async function () {
    global.browser = browser // need to patch global browser with mocked api
    browser.menus = browser.contextMenus // sinon-chrome doesn't wrap this method as it should
    
    sinon.stub(browser.i18n, 'getAcceptLanguages').resolves(['de-de', 'en-au'])
    sinon.stub(browser.i18n, 'getUILanguage').returns('en-en')

    languages = await getDefaultLanguages()
  })

  it('asserts that getDefaultLanguages() returns an array of strings', function () {
    assert.isTrue(languages.every(x => typeof x === 'string'))
  })

  it('asserts that getDefaultLanguages() includes UI and i18n languages', function () {
    assert.sameMembers(languages, ['de-de', 'en-en', 'en-au'])
  })
})

The tests fail due to both the stubbed methods returning undefined, but the Sinon docs state quite clearly that stub.resolves(value):

Causes the stub to return a Promise which resolves to the provided value.

When constructing the Promise, sinon uses the Promise.resolve method. You are responsible for providing a polyfill in environments which do not provide Promise. The Promise library can be overwritten using the usingPromise method.

Since node has built in Promise support, I would expect the above stubs to resolve with the values specified (an array of locale strings and a locale string), but both resolve/return undefined.

Would appreciate some help with this one!

Answer

Turns out sinon-chrome, for whatever reason, needs to register the ‘i18n’ plugin during runtime and before tests are run.

Why this specific part of the web-extensions API is not implemented the same way all the other mocks are remains a mystery, but adding two lines fixed the problem and allowed the sinon stubs to work as expected:

const sinon = require('sinon')
const browser = require('sinon-chrome/extensions')
const I18nPlugin = require('sinon-chrome/plugins').I18nPlugin // I18n plugin constructor
const { assert } = require('chai')
const helpers = require('../src/helpers')

// helpers that rely on the web-extension API (will need to be mocked)
describe('Helpers: Web-Extension API', function () {
  const { getDefaultLanguages } = helpers

  let languages

  before(async function () {
    global.browser = browser // need to patch global browser with mocked api
    browser.menus = browser.contextMenus // sinon-chrome doesn't wrap this method as it should
    browser.registerPlugin(new I18nPlugin()) // register the plugin on browser instance
    
    sinon.stub(browser.i18n, 'getAcceptLanguages').resolves(['de-de', 'en-au'])
    sinon.stub(browser.i18n, 'getUILanguage').returns('en-en')

    languages = await getDefaultLanguages()
  })
})

Sinon-chrome throws a type error if you try to spy on a non-existent object property, or if the given property is not a function. Unfortunately it does not throw any error and simply returns undefined if trying to stub a non-existent object property, which seems like a poor design choice.

That is why both the returns() and resolves() stubs, in the original code, returned undefined.



Source: stackoverflow