Mocking mongoose validators with jest - node.js

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.

Related

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();

Writing test for Joi validation

I have the following model:
const mongoose = require("mongoose");
const Joi = require("#hapi/joi");
const activitySchema = new mongoose.Schema({
title: {
type: String,
maxlength: 255,
minlength: 3,
required: true
}
});
const Activity = mongoose.model("Activity", activitySchema);
function validateActivity(activity) {
const schema = Joi.object({
title: Joi.string().min(3).max(255).required()
});
return schema.validate(activity)
}
module.exports.Activity = Activity;
module.exports.validate = validateActivity;
And I'm writing a unit test for the validateActivity function. I am not sure about what's the best practice for writing these tests.
So far I came up with:
const {validateActivity} = require("../../models/activity");
describe("activity model", () => {
let mockActivity;
beforeEach(() => {
mockActivity = {
title: "123"
}
});
it("should return an error if no title is provided", () => {
delete mockActivity.title;
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/any.required/);
});
it("should return an error if title is not a string", () => {
mockActivity.title = { test: "test" };
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.base/);
});
it("should return an error if title is less than 3 chars", () => {
mockActivity.title = "12";
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.min/);
});
it("should return an error if title is more than 255 chars", () => {
mockActivity.title = Array(258).join("a");
const result = validateActivity(mockActivity);
expect(result.error.details[0].type).toMatch(/string.max/);
});
});
So once I add several fields to my model the testing will be quite repetitive. Is it necessary to write a test for each scenario?
I guess it all depends on how strict and important you want your validation to be. If you have a data centric application where your bread and butter is data. if you don't test your data thoroughly you might end up in a bad situation.
Now imagine in your case you have a schema amd you don't have tests. If you for example remove max(255) condition, your all tests will pass (since you don't have any) and your customers will be able to insert data which is longer than 255 which obviously will be wrong.
I don't see any thing wrong in what you are doing. You can consider grouping stuff when you have multiple fields. In nutshell, you should test your schema thoroughly if data integrity is super important for you.
After viewing this comment https://stackoverflow.com/a/33588497/5733078 it seems that it would not make sense to check every scenario since we can trust that the validate function has already been tested within Joi.

Mocking Mongoose model with jest

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

Need a better way to fill values from req.body into a model

I am creating a webapp using the following stack:
Node
Express
MongoDB
Mongoose
I have structured the app into a MVC structure. In the app I need to get create (post) and update (put) data values which I get from res.body and copy them to Mongoose Model. For example I am doing the following:
Mongoose Model:
let mongoose = require('mongoose');
let customerPaymentType = mongoose.Schema({
type: { type: String, required: true, unique: true}
},
{
timestamps: true
}
);
module.exports = mongoose.model('CustomerPaymentType', customerPaymentType);
Controller (Only a part):
let mongoose = require('mongoose');
let CustomerPaymentType = mongoose.model('CustomerPaymentType');
class CustomerPaymentTypeController {
constructor(){}
create(req, res){
let customerPaymentType = new CustomerPaymentType();
this._setCustomerPaymentType(req.body, customerPaymentType);
customerPaymentType.save(error=>{
if (error) res.send(error);
res.json({
message: 'Customer payment type successfully created',
customerPaymentType:{_id: customerPaymentType._id}
});
});
}
//private methods
_setCustomerPaymentType(rawCustomerPaymentType, customerPaymentType){
if (typeof rawCustomerPaymentType.type !== 'undefined') customerPaymentType.type = rawCustomerPaymentType.type.trim();
}
}
module.exports = CustomerPaymentTypeController;
In this model there is only one field, thus populating the model with the data from the req.body in the controller file is easy. But I have other models with more than 30 fields, and it is taking long time to populate them. Is there any easier way to deal with repopulating a model, similar to one present in Ruby on Rails?

mocha --watch and mongoose models

If I leave mocha watching for changes, every time I save a file mongoose throws the following error:
OverwriteModelError: Cannot overwrite Client model once compiled
I know that mongoose won't allow to define a model twice, but I don't know how to make it work with mocha --watch.
// client.js
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var clientSchema = new Schema({
secret: { type: String, required: true, unique: true },
name: String,
description: String,
grant_types: [String],
created_at: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Client', clientSchema);
And here is the test
// client-test.js
var chai = require('chai');
var chaiHttp = require('chai-http');
var mongoose = require('mongoose');
var server = require('../../app');
var Client = require('../../auth/models').Client;
var should = chai.should();
chai.use(chaiHttp);
describe('client endpoints', function() {
after(function(done) {
mongoose.connection.close();
done();
});
it('should get a single client on /auth/client/{clientId} GET', function(done) {
var clt = new Client({
name: 'my app name',
description: 'super usefull and nice app',
grant_types: ['password', 'refresh_token']
});
clt.save(function(err) {
chai.request(server)
.get('/auth/client/' + clt._id.toString())
.end(function(err, res) {
res.should.have.status(200);
res.should.be.json;
res.body.should.have.property('client_id');
res.body.should.not.have.property('secret');
res.body.should.have.property('name');
res.body.should.have.property('description');
done();
});
});
});
});
I had the same issue. My solution was to check whether the model was created/compiled yet, and if not then do so, otherwise just retrieve the model.
using mongoose.modelNames() you can get an array of the names of your models. Then use .indexOf to check if the model you want to get is in the array or not. If it is not, then compile the model, for example: mongoose.model("User", UserSchema), but if it is already defined (as is the case with mocha --watch), simply retrieve the model (don't compile it again), which you can do with for example: mongoose.connection.model("User").
This is a function which returns a function to do this checking logic, which itself returns the model (either by compiling it or just retrieving it).
const mongoose = require("mongoose");
//returns a function which returns either a compiled model, or a precompiled model
//s is a String for the model name e.g. "User", and model is the mongoose Schema
function getModel(s, model) {
return function() {
return mongoose.modelNames().indexOf(s) === -1
? mongoose.model(s, model)
: mongoose.connection.model(s);
};
}
module.exports = getModel;
This means you have to require your model a bit differently, since you are likely replacing something like this:
module.exports = mongoose.model("User", UserSchema);
which returns the model itself,
with this:
module.exports = getModel("User", UserSchema);
which returns a function to return the model, either by compiling it or just retrieving it. This means when you require the 'User' model, you would want to call the function returned by getModel:
const UserModel = require("./models/UserModel")();
I hope this helps.
Here is a simpler code for the function getModel() that George is proposing
function getModel(modelName, modelSchema) {
return mongoose.models[modelName] // Check if the model exists
? mongoose.model(modelName) // If true, only retrieve it
: mongoose.model(modelName, modelSchema) // If false, define it
}
For a larger explanation on how to define and require the model, look here
Hope this helps :)
This worked for me,
place on the top of your test file:
const mongoose = require("mongoose");
mongoose.models = {};
mongoose.modelSchemas = {};

Resources