jhnns/rewire

Mongoose model overwrite error

csugden opened this issue ยท 12 comments

I've used rewire with a Mongoose model in a jasmine spec, and also used require to load the same model in a different spec. I get this error:

{ message: 'Cannot overwrite `Organisation` model once compiled.', name: 'OverwriteModelError' }

It looks like it's asking Mongoose to define the same model twice, whereas if we used require both times this does not happen.

Is there a workaround?

Thanks.

Mhmm interesting. I think this could be solved if you stored the rewired module under require.cache so node doesn't execute the code twice:

var myModel = rewire("./myModel");
require.cache[require.resolve("./myModel")] = myModel;

// than somewhere else
var myModel = require("./myModel"); // returns the rewired module

It's important that rewire is the first one that executes the module since rewire always creates a fresh instance. node's require always looks up the require.cache before evaling the code.

Hi, I've been trying this (I work with @csugden) and I'm getting undefined for the result of myModel. Specifically:

var rewire = require('../node_modules/rewire'),
  myModel = rewire('./lib/myModel');
require.cache[require.resolve('./lib/myModel')] = myModel;

// in another file
var myModel = require('./lib/myModel');
console.log(myModel); // undefined

As a follow-up, I noted that the object structure of what is put in the require cache by require is a bit different to what is put there by overwriting the cache. For example, modules in the cache that require has put there have a "paths" property.

Oh yes, you're right. require.cache stores the module, not module.exports. A quickfix would be:

var myModel = rewire('./lib/myModel');
require.cache[require.resolve('./lib/myModel')] = {
    exports: myModel
};

... thus faking the actual module-object. As you can see in node's source code, they're directly returning module.exports if module is defined.

But that's surely not a clean solution. Maybe rewire should provide a method to directly store the rewired module in the require.cache, like:

var myModel = rewire.global("./lib/myModel");

require("./lib/myModel") === myModel; // true
rewire("./lib/myModel") === myModel; // false

But what happens when the require.cache already contains an instance of the module? Is rewire allowed to replace the module? This could have serious implications...

Maybe there's also another solution? Any suggestions?

The quickfix you suggested worked. Jasmine-node seems to collect the spec files together alphabetically, so I've got "aFixSpec.js" that gets loaded first.

I also wanted to be able to run the tests independently, which meant keeping the use of rewire in the spec files. I got around this by using the same technique to overwrite require's cache of rewire and replace it with require (also in aFixSpec.js):

require.cache[require.resolve('rewire')] = {
  exports: require
};

This means when you run all the tests at once, any further use of rewire outside of aFixSpec.js uses require and hence goes through the same cache, which looks like a noop; but as I mentioned above, this allows the rewire method to remain in the test specs so they can be run independently.

Mhmm I don't get this fix ^^. If a test uses rewire() but receives require() instead, wouldn't that break the test? The returned module won't have a __set__- or __get__-method.

Another solution which came in my mind is to separate the registering-part from the definition. So instead of writing:

var Cat = mongoose.model('Cat', { name: String });

you would write

// CatSchema.js
module.exports = mongoose.Schema({
    name: String
});
// Cat.js
module.exports = mongoose.model('Cat', CatSchema);

I don't know what exactly you're trying to test and mock with rewire, but separating these two things is a good idea anyway.

Close?

ok!

For future googlers, you could also try using sinon to enable rewire usage with mongoose models. Here is an example that worked for me:

  function disableNewModelDefinitions() {
    var originalModel = mongoose.model;
    sinon.stub(mongoose, 'model', allowModelRetrieval);
    function allowModelRetrieval() {
      if (arguments.length === 1) {
        return originalModel.apply(mongoose, arguments);
      }
    }
  }
jhnns commented

Nice ๐Ÿ‘

@Trindaz Could you provide an example of how you actually use that function? Thanks!

I believe this would work in most scenarios where you need to re-register a mongoose model:

delete mongoose.connection.models['Cat'];
// proceed with rewire, proxyquire, etc.