Mocking Mongoose model with jest - node.js

I am trying to mock a mongoose model with jest, but is getting Cannot create property 'constructor' on number '1' error. I was able to reproduce the issue by creating the project with 2 files shown below. Is there a way to mock a mongoose model with jest?
./model.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const schema = new Schema({
name: String
})
module.exports = mongoose.model('Test', schema)
./model.test.js
jest.mock('./model')
const Test = require('./model')
// Test.findOne.mockImplementation = () => {
// ...
// }
Error:
FAIL ./model.test.js
● Test suite failed to run
TypeError: Cannot create property 'constructor' on number '1'
at ModuleMockerClass._generateMock (../../jitta/sandbox/rest_api/node_modules/jest-mock/build/index.js:458:34)
at Array.forEach (native)
at Array.forEach (native)
at Array.forEach (native)
Update:
Seems to be a bug in jest.
https://github.com/facebook/jest/issues/3073

An other solution is to spyOn the model prototype functions.
For example, this will make MyModel.save() fail :
jest.spyOn(MyModel.prototype, 'save')
.mockImplementationOnce(() => Promise.reject('fail update'))
You can use mockImplementationOnce to not having to mockRestore the spy. But you can also use mockImplementation and use something like :
afterEach(() => {
jest.restoreAllMocks()
})
Tested with "mongoose": "^4.11.7" and "jest": "^23.6.0".

ok, i had the same problem so i author this package to solve this problem:
https://www.npmjs.com/package/mockingoose
this is how you can use it let's say this is your model:
import mongoose from 'mongoose';
const { Schema } = mongoose;
const schema = Schema({
name: String,
email: String,
created: { type: Date, default: Date.now }
})
export default mongoose.model('User', schema);
and this is your test:
it('should find', () => {
mockingoose.User.toReturn({ name: 2 });
return User
.find()
.where('name')
.in([1])
.then(result => {
expect(result).toEqual({ name: 2 });
})
});
checkout the tests folder for more examples:
https://github.com/alonronin/mockingoose/blob/master/___tests___/index.test.js
no connections is made to the database!

for typescript I found a hack that works
jest.spyOn(model, 'find').mockReturnValueOnce(dummyData as any);

Mockingoose seems to be a very nice solution. But I was also able to mock my model with Jest.mock() function. At least create method.
// in the module under the test I am creating (saving) DeviceLocation to DB
// someBackendModule.js
...
DeviceLocation.create(location, (err) => {
...
});
...
DeviceLocation model definition:
// DeviceLocation.js
import mongoose from 'mongoose';
const { Schema } = mongoose;
const modelName = 'deviceLocation';
const DeviceLocation = new Schema({
...
});
export default mongoose.model(modelName, DeviceLocation, modelName);
DeviceLocation mock in the __mocks__ folder in the same folder as DeviceLocation model:
// __mock__/DeviceLocation.js
export default {
create: (x) => {
return x;
},
};
in the test file:
// test.js
// calling the mock
...
jest.mock('../../src/models/mongoose/DeviceLocation');
import someBackendModule from 'someBackendModule';
...
// perform the test

Related

Cannot read property 'create' of undefined in expressjs

Please am new to Nodejs but am trying to insert a data into my database using sequelize but am getting Cannot read property .create of undefined.
This is my index.js file
const fs = require('fs')
const path = require('path')
const Sequelize = require('sequelize')
const config = require('../config/config')
const db = {}
const sequelize = new Sequelize(
config.DB,
config.USER,
config.PASSWORD,
{
host: config.HOST,
dialect: config.dialect,
operatorsAliases: false,
pool: {
max: config.pool.max,
min: config.pool.min,
acquire: config.pool.acquire,
idle: config.pool.idle
}
});
fs
.readdirSync(__dirname)
.filter((file) =>
file !== 'index.js'
)
.forEach((file) => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes)
// db[model] = model
db.User = model
})
db.sequelize = sequelize
db.Sequelize = Sequelize
module.exports = db;
This is my Model/User.js File
module.exports = (sequelize, DataTypes) => {
sequelize.define('User', {
hmtec_email: {
type: DataTypes.STRING,
unique: true
},
hmtec_password: DataTypes.STRING
})
This is my Controllers/AuthController.js File
const {User} = require ('../models/User')
module.exports = {
async register (req, res) {
try {
const user = await User.create(req.body)
res.send(user.toJSON())
} catch (err) {
console.log(err);
res.status(400).send({
error: 'Email already in Use'
})
}
}
I don't know what wrong with the code, I dont know why am getting that error of .create is undefined
I think your problem is in the last file .. AuthController.js
const {User} = require ('../models/User')
You are using the Es6 new destruction assignment
More Info here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
Normally you should define your variable without the brackets
That mean you got all of the variable available.
But adding the brackets means that you want to get the child of the object, this child is named user .. and then name the variable also as user
and search these info
From the require file after the equal.
But in your user file .. you are exporting a function .. that does not have any child named user
Thus undefined
Better alternative is to use Classes
More info here : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
So in user model
// Inside your function you need to use sequalize module , iam not familar with it but you must require it to use it
const Sequelize = require('sequelize');
// Define the Class
class user {
// add the name you want to use for the function - here I used create so the other code also work - also I inserted async because this is asynchronous and also I removed the sequalize from parameters because you don't need it now after it is required above.
async create (DataTypes) => {
await Sequelize.define('User', {
hmtec_email: {
type: DataTypes.STRING,
unique: true
},
hmtec_password: DataTypes.STRING
})
}
module.exports = user;
Now when you require the file in Authcontroller , remove the brackets
const User = require ('../models/User')
and now you can use User.create(YOUR_DATA_VARIABLE);in your authController.js
and if there is any other functions inside this class you also can use them in the same manner.
I hope this fix your problem.

Cannot stub non-existent property "create" of a mongoose document

I'm writing unit tests for my express app using Sinon. I have a Log model:
import { Schema, model } from 'mongoose';
const LogSchema = new Schema({
content: {
type: String,
required: true,
},
date: {
type: Date,
default: Date.now,
},
});
const Log = model('Log', LogSchema);
export default Log;
And a LogController:
import Log from '../models/Log';
class LogController {
static async create(content) {
await Log.create({ content });
}
}
export default LogController;
I'm trying to write tests for LogController.create().
import { createSandbox } from 'sinon';
import Log from '../../../src/models/Log';
import LogController from '../../../src/controllers/LogController';
describe('LogController', () => {
let sandbox;
let createStub;
beforeEach(() => {
sandbox = createSandbox();
createStub = sandbox.stub(Log, 'create');
});
describe('create()', () => {
it('should create a Log with the given content', async () => {
await LogController.create('Bob Lob Law is on the house');
expect(createStub.calledWith({ content: 'Bob Lob Law is on the house' })).to.be.true;
});
});
But then I get TypeError: Cannot stub non-existent property create, implying that Log doesn't have a create method. Which is weird, since I have other controllers that are tested exactly like this and they don't throw any error. I also tried stubbing it with Log.create = sandbox.stub() but I got the same error. Maybe I'm doing something wrong on my model definition? How can I fix this?
Sinon thinks you're trying to stub object.create for some reason (instead of model.create) possibly because model.create returns a promise, but your stub doesn't return anything. You need to change
createStub = sandbox.stub(Log, 'create');
to
createStub = sandbox.stub(Log, 'create').resolves();

Jest not closing because of Mongoose.model

I am trying to create my first test with jest.
user_model_test.js
const mongoose = require('mongoose')
const User = require('../user_model')
describe('user model tests', () => {
beforeAll( async () => {
await mongoose.connect('mongodb://localhost/supertest21')
})
afterAll( async () => {
await mongoose.connection.close()
})
it("has a module", () => {
expect(User).toBeDefined()
})
})
user_model.js
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const userSchema = new Schema({
username: {
type: String,
required: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: true
}
})
const User = mongoose.model('User', userSchema, 'user')
module.exports = User
When I run my test, running with --detectOpenHandles I get this error:
Jest has detected the following 1 open handle potentially keeping Jest from exiting:
● PROMISE
17 | })
18 |
> 19 | const User = mongoose.model('User', userSchema, 'user')
| ^
20 |
21 | module.exports = User
at Function.init (node_modules/mongoose/lib/model.js:970:16)
at Mongoose.Object.<anonymous>.Mongoose.model (node_modules/mongoose/lib/index.js:396:11)
at Object.<anonymous> (libs/user/user_model.js:19:23)
at Object.<anonymous> (libs/user/__tests__/user_model_test.js:3:14)
I know that there is something to do with the mongoose.model initialization.When I pass the 4th parameter to mongoose.model, to skip initialization, the promise error don't show up but the test never closes and don't show any more errors. Any ideas?
Using a setup file and setupFilesAfterEnv
Jest can call a "setup.js" file to run some beforeAll and afterAll functions. As ongoing Mongoose's connections keep Jest open, we will close them using the afterAll hook.
Solution
In your jest.config.js, add the following lines:
setupFilesAfterEnv: [
'<rootDir>/tests/setup.js', // <- Feel free to place this file wherever it's convinient
],
Create a setup.js file with the following code:
import { getMongoDBInstance } from '../src/bin/server.ts';
afterAll(async () => {
const mongoDB = getMongoDBInstance();
await mongoDB.connection.close();
});
Here, getMongoDBInstance is a function that returns me the instance of Mongoose I instantiate when my server boots.
let mongoDB;
async function initServer() {
...
await mongoose.connect(uristring, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoDB = mongoose;
...
}
export const getMongoDBInstance = () => mongoDB;
And now after all tests ran, Jest will call this function and close any MongoDB connections! You can follow the same to solve open connections with knex, or any other node ORMs.
Try changing await mongoose.connection.close() to await mongoose.disconnect();. Worked for me.

Mocking mongoose validators with jest

I'm testing a mongoose model's validation, while trying to mock a validator, the model still has the reference to the original function, so the validation keeps calling the original function.
I want to test that the validator function get's called, however since the validator goes to the db I need to mock it.
This is my model:
const { hasRequiredCustoms } = require('../utils/validators')
const ProductSchema = new Schema({
customs: {
type: [String],
validate: hasRequiredCustoms // <- This is the validator
}
})
const Product = mongoose.model('Product', ProductSchema)
module.exports = Product
The original validators:
module.exports = {
hasRequiredCustoms(val) {
console.log('Original')
// validation logic
return result
},
//etc...
}
This is the mock for validators:
const validators = jest.genMockFromModule('../validators')
function hasRequiredCustoms (val) {
console.log('Mock')
return true
}
validators.hasRequiredCustoms = hasRequiredCustoms
module.exports = validators
And the test:
test('Should be invalid if required customs missing: price', done => {
jest.mock('../../utils/validators')
function callback(err) {
if (!err) done()
}
const m = new Product( validProduct )
m.validate(callback)
})
Every time I run the tests the console logs the Original. Why is the reference still going back to the original module? seems like I'm missing some super essential concept of how require works or the way mongoose stores the validators references.
Thanks for the help.

Sequelize: Require vs Import

In the documentation for sequlize they use the import function like so
// in your server file - e.g. app.js
var Project = sequelize.import(__dirname + "/path/to/models/project")
// The model definition is done in /path/to/models/project.js
// As you might notice, the DataTypes are the very same as explained above
module.exports = function(sequelize, DataTypes) {
return sequelize.define("Project", {
name: DataTypes.STRING,
description: DataTypes.TEXT
})
}
However, what would be so wrong with this?
// in your server file - e.g. app.js
var Project = require(__dirname + "/path/to/models/project")
// The model definition is done in /path/to/models/project.js
var Project = sequelize.define("Project", {
name: Sequelize.STRING,
description: Sequelize.TEXT
});
module.exports = Project
Well, as you can see your model definition needs two things:
Sequelize or DataTypes
sequelize
In your first example when using sequelize.import('something'); it is similar to use require('something')(this, Sequelize); (this being the sequelize instance)
Both are necessary to initialize your model, but the important thing to understand is: One of these is a classtype so it's global, the other one is an instance and has to be created with your connection parameters.
So if you do this:
var Project = sequelize.define("Project", {
name: Sequelize.STRING,
description: Sequelize.TEXT
});
module.exports = Project
Where does sequelize come from? It has to be instantiated and passed somehow.
Here is an example with require instead of import:
// /path/to/app.js
var Sequelize = require('sequelize');
var sequelize = new Sequelize(/* ... */);
var Project = require('/path/to/models/project')(sequelize, Sequelize);
// /path/to/models/project.js
module.exports = function (sequelize, DataTypes) {
sequelize.define("Project", {
name: DataTypes.STRING,
description: DataTypes.TEXT
});
};
module.exports = Project
You could even change it so you wouldn't have to pass Sequelize by requiring it in the model itself, but you would still need to create a sequelize instance prior to define the model.
sequelize.import is deprecated as of sequelize 6
As mentioned at https://sequelize.org/master/manual/models-definition.html
Deprecated: sequelize.import
Note: You should not use sequelize.import. Please just use require instead.
So you should just port:
// index.js
const sequelize = new Sequelize(...)
const User = sequelize.import('./user')
// user.js
module.exports = (sequelize, DataTypes) => {
// Define User.
return User;
}
to:
// index.js
const sequelize = new Sequelize(...)
const User = require('./user')(sequelize)
// user.js
const { DataTypes } = require('sequelize')
module.exports = (sequelize) => {
// Define User.
return User;
}

Resources