Sinon: Sandbox throws error 'cannot stub non-existent own property'

Created on 18 Aug 2017  ·  14Comments  ·  Source: sinonjs/sinon

I just tried upgrading from 2.4.1 to 3.2.1 and encountered the following issue. This code works in 2.4.1:

        const spy = sandbox.spy();
        sandbox.stub(window, 'google').value({
            maps: {
                LatLng: x => x,
                Map: spy
            }
        });

But in 3.2.1 it throws an exception: TypeError: Cannot stub non-existent own property google

It's not mentioned in the migration guide so it appears to be a regression.

Bug Regression

Most helpful comment

~Thank you for restoring this behavior.~

Wanted to add a use case that supports stubbing nonexistint properties.

In my use case, I'm stubbing a property on a config object. The config object has various optional keys, and is initialized by loading a file from the developer's machine. When I run a particular test, I need one of those keys set to a known value, and then want to restore the developer's object as it was.

sandbox.stub(serverSecrets, 'the_key_i_need_set').value(fakeValue) is a very clear way to convey this. It's good that I get the same behavior, _even though I don't know at runtime whether or not the key is set_.

All 14 comments

Related to #1512.

If the property doesn't exist you don't need to add it to the sandbox. Simply overwrite it. But yeah, if it worked before, and we haven't explicitly said it should change, then it's a regression.

Not sure what we should do here. Update the docs to say stubbing non-existing values doesn't make sense and is unsupported, or make it possible?

If the property doesn't exist you don't need to add it to the sandbox. Simply overwrite it.

The nice thing about adding the property to the sandbox is that sinon then helps me keep my global test environment clean between each test via sandbox.restore(). It is an extremely useful feature, especially when dealing with 3rd party libraries like Google Maps where I don't control the API. It would be great if it could be made to work in the 3.x line.

Also I just noticed that I committed the sin of not providing a complete example. My sandbox is being created with 2.4.1 format:

let sandbox;

before(() => { sandbox = sinon.sandbox.create(); })
afterEach(() => { sandbox.restore(); })

Not sure if that's important; apologies for not providing it sooner.

I think that in the scenarios like the one @ZebraFlesh describes, I would prefer to have the text fixture be more explicit.

// not so explicit, doesn't work with [email protected]
beforeEach(function() {
    const spy = sandbox.spy();
    sandbox.stub(window, 'google').value({
        maps: {
            LatLng: x => x,
            Map: spy
        }
    }); 
});
// more explicit, works with sinon@2, sinon@3
function setGoogleMapsFixture(sandbox) {
    window.google = {
        maps: {
            LatLng: x => x,
            Map: sandbox.spy()
        }
    };
}

function removeGoogleMapsFixture() {
    delete window.google;
}

beforeEach(function() {
    setGoogleMapsFixture(sandbox)
});

// not using afterEach, as this only needs to happen
// after the last test in this block is run
after(function() {
    removeGoogleMapsFixture();
});

With more explicit setting of the fixture like outlined above, you don't need a feature in Sinon that would allow stubbing of non-existing own properties.

Not sure what we should do here. Update the docs to say stubbing non-existing values doesn't make sense and is unsupported, or make it possible?

While I recognise that it might be convenient in some scenarios (like the one described by @ZebraFlesh), I think that stubbing non-existing own properties is likely to lead to mistakes in tests, where the test passes because the author mistyped the name of the existing property they were aiming to stub. We should aim to eliminate the possibility of mistakes where we can without being too restrictive.

I think stubbing non-existing own properties should remain unsupported. We should update the documentation.

@mroderick I concur with you in that it might make for less bugs, but we already support this for normal stubs. If we should stop supporting this behavior, we need to remove it there as well, to be consistent. It would be strange to only support this feature outside of sandboxes, as sandboxes usually _add_ some possibilities. And removing the support is a breaking feature, so a major bump would be required as well.

So either:

  • remove the check promptly for sandboxes to fix this breaking feature

or/and(?)

  • remove the functionality for both normal and sandboxed stubs

    • release new major version with updated docs

With more explicit setting of the fixture like outlined above, you don't need a feature in Sinon that would allow stubbing of non-existing own properties.

I think that works well if your fixture never varies among your tests. However, my fixture does. A simple example is covering both success and failure cases:

it('handles the success case', () => {
        const spy = sandbox.spy();
        sandbox.stub(window, 'google').value({
            maps: {
                LatLng: x => x,
                Map: spy
            }
        });
        // ... test, including asserting that the spy was called
});

it('handles the failure case', () => {
        const msg = 'test error';
        sandbox.stub(window, 'google').value({
            maps: {
                LatLng: x => x,
                Map: sandbox.stub().throws(new Error(msg))
            }
        });
        // ... test, ignoring spy calls and instead focusing on error handling
});

The behavior in 2.x has the advantage of everything being properly cleaned up after each test via sandbox.restore(). Using the more explicit fixture setup example outlined above, I suppose you could delete the non-own property in an afterEach hook to achieve the same effect.

To solve the problem of introducing potential mistakes by inadvertently typing the name of an existing own property, sinon could modify the public API:

  • stub.ownValue(): stubs only own properties, throws for non-own properties
  • stub.value(): stubs only non-own properties, throws for own properties

The API becomes more explicit and the consumer is forced to choose the appropriate tool for the task at hand.

This is very much related to the discussion in #1508 (although it deals with normal stubs)h, where @lucasfcosta has the opposing view - that we _should not throw_ for undefined properties. Whatever we land on, I strongly believe _we need to be consistent_ in the stubbing APIs for normal stubs and sandboxes. We should not support it in one case, and not the other.

Right now, the situation is:

  • normal stubs used to throw in 1.x, but this changed in 2.0 and does not throw now anymore
  • sandboxes did not use to throw, but started throwing in 3.1(?)

So for a while we had feature parity, but then we lost it again ... I don't think this zig-zagging is very beneficial for the users, so we should land this discussion. While I do agree with Morgan in that it might make for more specific tests, I don't like dropping a behavior for two major releases, then re-adding it again. I think it would make the least noise (fixes for clients, questions/issues on this tracker) just to revert this regression.

While I understand the inconvenience, it seems like there's an easy workaround with minimal code changes.

before(function() {
  window.google = 'This is a placeholder for sinon to overwrite.';
});

after(function() {
  delete window.google;
});

This allows the sinon code to stay unchanged.

Regression vs poor documentation

This appears to be expected behavior since there are tests for it. We should update the docs to reflect that in my opinion. It came in a major so breaking changes are tolerated.

@fearphage Keeping the status quo means stubbing non-existing fields is unsupported behavior for sandboxes, while it is supported behavior for normal stubs. Isn't a bit unfortunate that the two feature sets don't align?

Resolution was implemented in #1557

I've read the various threads and I can see why this happened, but it's a real pain in Typescript where you've often got functions that are implemented on a class prototype, in which case sinon spits the dummy even though everything looks fine type wise (since keyof YourType will happily allow all public functions which are defined further down the prototype chain).

I get that Typescript probably isn't a priority for you guys, but even in JS it seems counter intuitive that myObject.callMe() will execute perfectly happily, while sinon.stub(myObject, "callMe") won't in that case. I'd prefer not to have to go and investigate how that particular object was put together just so I know how to stub it.

I really think this is an important use case to make a happy path for, considering classes are getting more native support in JS.

If you get an error saying that the method is undefined on the object, you know the error is probably up the prototype. Then directly modifing the object using myObject.callMe = sinon.stub(); doesn't seem like that much of a hassle IMHO ... Should also save you from creating cleanup/teardown functions, as the prototype was never changed.

Yeah, I guess it's not overly difficult to work around, it's just putting more cognitive load on me to be aware of how things are implemented.

It also just seems unexpected, so that I felt the need to add a comment in the test to explain why the stubbing code on 2 consecutive lines for the same object was different, and why I was manually deleting one of the stubs in my teardown, but the other was handled by the sandbox.

~Thank you for restoring this behavior.~

Wanted to add a use case that supports stubbing nonexistint properties.

In my use case, I'm stubbing a property on a config object. The config object has various optional keys, and is initialized by loading a file from the developer's machine. When I run a particular test, I need one of those keys set to a known value, and then want to restore the developer's object as it was.

sandbox.stub(serverSecrets, 'the_key_i_need_set').value(fakeValue) is a very clear way to convey this. It's good that I get the same behavior, _even though I don't know at runtime whether or not the key is set_.

Was this page helpful?
0 / 5 - 0 ratings