Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restoring stub fails when it executed .value() #2226

Closed
XxshiftxX opened this issue Feb 24, 2020 · 6 comments
Closed

Restoring stub fails when it executed .value() #2226

XxshiftxX opened this issue Feb 24, 2020 · 6 comments
Assignees

Comments

@XxshiftxX
Copy link

XxshiftxX commented Feb 24, 2020

Describe the bug
I made a stub with mongoose.connection's readyState Property.
And I executed sinon.restore() when test finished (with afterEach)

It throws TypeError when my stub restored.

To Reproduce

  1. Required 2 modules: mongoose, sinon
  2. Make stub with this code.
sinon.stub(mongoose.connection, 'readyState').value(1);
  1. Restore stub with sinon.restore().
  2. It throws error.
D:\dev\personal\sinon-test\node_modules\sinon\lib\sinon\stub.js:103
                Object.defineProperty(object, property, actualDescriptor);
                       ^

TypeError: Cannot redefine property: readyState
    at Function.defineProperty (<anonymous>)
    at Function.restore (D:\dev\personal\sinon-test\node_modules\sinon\lib\sinon\stub.js:103:24)
    at D:\dev\personal\sinon-test\node_modules\sinon\lib\sinon\sandbox.js:30:21
    at Array.filter (<anonymous>)
    at applyOnEach (D:\dev\personal\sinon-test\node_modules\sinon\lib\sinon\sandbox.js:29:5)
    at Sandbox.restore (D:\dev\personal\sinon-test\node_modules\sinon\lib\sinon\sandbox.js:164:9)
    at Object.<anonymous> (D:\dev\personal\sinon-test\test.js:11:7)
    at Module._compile (internal/modules/cjs/loader.js:959:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:995:10)
    at Module.load (internal/modules/cjs/loader.js:815:32)
    at Function.Module._load (internal/modules/cjs/loader.js:727:14)
    at Function.Module.runMain (internal/modules/cjs/loader.js:1047:10)
    at internal/main/run_main_module.js:17:11

Expected behavior
I tested this case with this code:

const sinon = require('sinon');
const mongoose = require('mongoose');

console.log(mongoose.connection.readyState);

sinon.stub(mongoose.connection, "readyState").value(1);

console.log(mongoose.connection.readyState);

sinon.restore();

console.log(mongoose.connection.readyState);

and It should prints 0, 1, 0.
but It throws error above before second 0 printed

Screenshots
image

Context (please complete the following information):

  • Library version: ^9.0.0
  • Environment: Windows 10 & node.js v12.13.1
  • Other libraries you are using: mongoose
@fatso83 fatso83 self-assigned this Feb 24, 2020
XxshiftxX pushed a commit to online-fiasco/playset that referenced this issue Feb 24, 2020
@mroderick
Copy link
Member

Thank you for reporting this issue.

Here's what I've learned so far in my investigation of this

get vs. defineProperty

From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#get_vs._defineProperty

While using the get keyword and Object.defineProperty() have similar results, there is a subtle difference between the two when used on classes.

When using get the property will be defined on the instance's prototype, while using Object.defineProperty() the property will be defined on the instance it is applied to.

Assuming I've understood the above correctly, then the authors of Mongoose are trying to get the behaviour described for classes, in a file that doesn't use the class keyword. Considering they want the code to also work in browsers, which might not all support the class that's a reasonable choice.

That means that these are equivalent:

// ES2015
class Connection() {
    this._readyState = 0;

    get: function() {
        return this._readyState;
    },
    set: function(val) {
        this._readyState = val;
    }
}


// ES5.1
function Connection() {
    this._readyState = 0;
}

Object.defineProperty(Connection.prototype, "readyState", {
    get: function() {
        return this._readyState;
    },
    set: function(val) {
        this._readyState = val;
    }
});

Any solution for the issue, will have to be able to support both ways of creating accessors, on the prototype (as above) and on instances (see below).

// constructor
function Connection() {
    this._readyState = 0;
}

// instance
var connection = new Connection();

// expanding the instance with accessors, these won't exist on the prototype
Object.defineProperty(connection, "readyState", {
    get: function() {
        return this._readyState;
    },
    set: function(val) {
        this._readyState = val;
    },
    configurable: false
});

@mroderick
Copy link
Member

The existing test at https://github.com/sinonjs/sinon/blob/master/test/stub-test.js#L3453-L3464 is a bit misleading, as it doesn't actually test restoring the stub.

Adding a stub.restore() to the end of that test, causes the same error to be thrown.

@fatso83 fatso83 assigned mroderick and unassigned fatso83 Feb 24, 2020
@mantoni
Copy link
Member

mantoni commented Feb 24, 2020

More experiments:

This fails:

var o = {};
Object.defineProperty(o, "test", { value: 1 });
Object.defineProperty(o, "test", { value: 2 });

This works:

var o = {};
Object.defineProperty(o, "test", { writable: true, value: 1 });
Object.defineProperty(o, "test", { writable: true, value: 2 });

@mantoni
Copy link
Member

mantoni commented Feb 24, 2020

Here is clearly something wrong with property replacement / restoring:

'use strict';

const sinon = require('sinon');

const proto = {};
const obj = {};
Object.setPrototypeOf(obj, proto);
Object.defineProperty(proto, 'test', { writable: true, value: 1 });

sinon.replace(obj, 'test', 2);
console.log(Object.getOwnPropertyDescriptor(obj, 'test'));

sinon.restore();
console.log(Object.getOwnPropertyDescriptor(obj, 'test'));

Prints

{ value: 2, writable: true, enumerable: true, configurable: true }
{ value: 1, writable: true, enumerable: false, configurable: false }

So after the restore() call, we have a new property on obj with the descriptor of proto.test.

fatso83 added a commit to fatso83/sinon that referenced this issue Mar 7, 2020
This is basically the restoration of prototype props
@fatso83 fatso83 self-assigned this Mar 8, 2020
fatso83 added a commit to fatso83/sinon that referenced this issue Mar 9, 2020
@fatso83
Copy link
Contributor

fatso83 commented Mar 9, 2020

Simple fix with a discussion of doing some (separate) deeper changes in #2237

fatso83 added a commit to fatso83/sinon that referenced this issue Mar 9, 2020
@fatso83 fatso83 closed this as completed in 1fe433e Mar 9, 2020
@mroderick
Copy link
Member

The fix has been published to npm as sinon@9.0.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants