Multiple Sinon stubs against global window not working as expected

Viewed 1071

I'm using Karma with Mocha, Chai and Sinon to test code in a project using this boilerplate. The Subject Under Test uses the Speech Synthesis API.

I start by establishing window.speechSynthesis.getVoices in a beforeEach method

beforeEach(() => {
    global.window.speechSynthesis = {
        getVoices: () => (null),
    };
});

Then I have two test cases, in each one, I want to test what happens when a different set of voices is returned. To accomplish this I'm using Sinon stubs

First test case

it('supports speech and locale', () => {
    const getVoicesStub = sinon.stub(
        global.window.speechSynthesis,
        'getVoices');

    getVoicesStub.callsFake(() => (
        [{lang: 'en_US'}]
    ));

Second test case

it('will choose best matching locale', () => {
    const getVoicesStub = sinon.stub(
        global.window.speechSynthesis,
        'getVoices');

    getVoicesStub.callsFake(() => (
        [{lang: 'es_MX'}, {lang: 'es_US'}]
    ));

The problem is, when the SUT calls window.speechSynthesis.getVoices during the second test case, it's getting the results from the first stub. It's as if the second stub is doing nothing...

If I comment out the first test case, the second test case succeeds, but if I leave them both in, the second one fails because the wrong set of voices are being returned.

Any idea how to get the second stub to work as expected?

2 Answers

Your stub is not destroyed between tests. You need to restore the default function after a test and create your stub only once in before

describe("Test suite", () => {

    let getVoicesStub;

    before(() => {
        // executes before suite starts
        global.window.speechSynthesis = {
            getVoices: () => (null),
        };

        getVoicesStub = sinon.stub(global.window.speechSynthesis, 'getVoices');
    });

    afterEach(() => {
        // executes after each test
        getVoicesStub.restore();
    });

    it('supports speech and locale', () => {
        getVoicesStub.callsFake(() => ([{lang: 'en_US'}]));
    });

    it('will choose best matching locale', () => {
        getVoicesStub.callsFake(() => ([{lang: 'es_MX'}, {lang: 'es_US'}]));
    });
});

First off, big-ups to @Troopers. Just adding this answer to share the final solution and details I noticed along the way.


The real trick was adding a test-suite-level variable let getVoicesStub, then defining an afterEach method to restore the original function

afterEach(() => {
    getVoicesStub.restore();
});

A subtle caveat to @Troopers suggestion about defining the stub in a before method -

If the stub is defined outside of the test cases, I have to use a beforeEach, if the stub is defined within the test cases, I have to use a before method.

In both cases, the afterEach is critical! I've settled on the beforeEach solution as the stub is only defined in one place so there's slightly less code.

describe('Browser Speech', () => {
    let getVoicesStub;

    beforeEach(() => {
        global.window.speechSynthesis = {
            getVoices: () => (null),
        };

        getVoicesStub = sinon.stub(
            global.window.speechSynthesis,
            'getVoices');
    });

    afterEach(() => {
        getVoicesStub.restore();
    });

    it('supports speech and locale', () => {
        getVoicesStub.callsFake(() => (
            [{lang: 'en_US'}]
        ));

        // test case code ..
    });

    it('will choose best matching locale', () => {
        getVoicesStub.callsFake(() => (
            [{lang: 'es_MX'}, {lang: 'es_US'}]
        ));


        // test case code ..
    });
});
Related