Given a simple Mongoose model:
import mongoose, { Schema } from 'mongoose';
const PostSchema = Schema({
title: { type: String },
postDate: { type: Date, default: Date.now }
}, { timestamps: true });
const Post = mongoose.model('Post', PostSchema);
export default Post;
I wish to test this model, but I'm hitting a few roadblocks.
My current spec looks something like this (some stuff omitted for brevity):
import mongoose from 'mongoose';
import { expect } from 'chai';
import { Post } from '../../app/models';
describe('Post', () => {
beforeEach((done) => {
mongoose.connect('mongodb://localhost/node-test');
done();
});
describe('Given a valid post', () => {
it('should create the post', (done) => {
const post = new Post({
title: 'My test post',
postDate: Date.now()
});
post.save((err, doc) => {
expect(doc.title).to.equal(post.title)
expect(doc.postDate).to.equal(post.postDate);
done();
});
});
});
});
However, with this I'm hitting my database every time I run the test, which I would prefer to avoid.
I've tried using Mockgoose, but then my test won't run.
import mockgoose from 'mockgoose';
// in before or beforeEach
mockgoose(mongoose);
The test gets stuck and throws an error saying: Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test. I've tried increasing the timeout to 20 seconds but that didn't solve anything.
Next, I threw away Mockgoose and tried using Sinon to stub the save call.
describe('Given a valid post', () => {
it('should create the post', (done) => {
const post = new Post({
title: 'My test post',
postDate: Date.now()
});
const stub = sinon.stub(post, 'save', function(cb) { cb(null) })
post.save((err, post) => {
expect(stub).to.have.been.called;
done();
});
});
});
This test passes, but it somehow doesn't make much sense to me. I'm quite new to stubbing, mocking, what have you, ... and I'm not sure if this is the right way to go. I'm stubbing the save method on post, and then I'm asserting it to have been called, but I'm obviously calling it... Also, I can't seem to get to the arguments the non-stubbed Mongoose method would return. I would like to compare the post variable to something the save method returns, like in the very first test where I hit the database. I've tried a couple of methods but they all feel quite hackish. There must be a clean way, no?
Couple of questions:
Should I indeed avoid hitting the database like I've always read everywhere? My first example works fine and I could clear the database after each run. However, it doesn't really feel right to me.
How would I stub the save method from the Mongoose model and make sure it actually tests what I want to test: saving a new object to the db.
The basics
In unit testing one should not hit the DB. I could think of one exception: hitting an in-memory DB, but even that lies already in the area of integration testing as you would only need the state saved in memory for complex processes (and thus not really units of functionality). So, yes no actual DB.
What you want to test in unit tests is that your business logic results in correct API calls at the interface between your application and the DB. You can and probably should assume that the DB API/driver developers have done a good job testing that everything below the API behaves as expected. However, you also want to cover in your tests how your business logic reacts to different valid API results such as successful saves, failures due to data consistency, failures due to connection issues etc.
This means that what you need and want to mock is everything that is below the DB driver interface. You would, however, need to model that behaviour so that your business logic can be tested for all outcomes of the DB calls.
Easier said than done because this means you need to have access to the API via the technology you use and you need to know the API.
The reality of mongoose
Sticking to the basics we want to mock the calls performed by the underlying 'driver' that mongoose uses. Assuming it is node-mongodb-native we need to mock out those calls. Understanding the full interplay between mongoose and the native driver is not easy, but it generally comes down to the methods in mongoose.Collection because the latter extends mongoldb.Collection and does not reimplement methods like insert. If we are able to control the behaviour of insert in this particular case, then we know we mocked out the DB access at the API level. You can trace it in the source of both projects, that Collection.insert is really the native driver method.
For your particular example I created a public Git repository with a complete package, but I will post all of the elements here in the answer.
The solution
Personally I find the "recommended" way of working with mongoose quite unusable: models are usually created in the modules where the corresponding schemas are defined, yet they already need a connection. For purposes of having multiple connections to talk to completely different mongodb databases in the same project and for testing purposes this makes life really hard. In fact, as soon as concerns are fully separated mongoose, at least to me, becomes nearly unusable.
So the first thing I create is the package description file, a module with a schema and a generic "model generator":
package.json
{
"name": "xxx",
"version": "0.1.0",
"private": true,
"main": "./src",
"scripts": {
"test" : "mocha --recursive"
},
"dependencies": {
"mongoose": "*"
},
"devDependencies": {
"mocha": "*",
"chai": "*"
}
}
src/post.js
var mongoose = require("mongoose");
var PostSchema = new mongoose.Schema({
title: { type: String },
postDate: { type: Date, default: Date.now }
}, {
timestamps: true
});
module.exports = PostSchema;
src/index.js
var model = function(conn, schema, name) {
var res = conn.models[name];
return res || conn.model.bind(conn)(name, schema);
};
module.exports = {
PostSchema: require("./post"),
model: model
};
Such a model generator has its drawbacks: there are elements that may need to be attached to the model and it would make sense to place them in the same module where the schema is created. So finding a generic way to add those is a bit tricky. For example, a module could export post-actions to be automatically run when a model is generated for a given connection etc. (hacking).
Now let's mock the API. I'll keep it simple and will only mock what I need for the tests in question. It is essential that I would like to mock out the API in general, not individual methods of individual instances. The latter might be useful in some cases, or when nothing else helps, but I would need to have access to objects created inside of my business logic (unless injected or provided via some factory pattern), and this would mean modifying the main source. At the same time, mocking the API in one place has a drawback: it is a generic solution, which would probably implement successful execution. For testing error cases, mocking in instances in the tests themselves could be required, but then within your business logic you might not have direct access to the instance of e.g. post created deep inside.
So, let's have a look at the general case of mocking successful API call:
test/mock.js
var mongoose = require("mongoose");
// this method is propagated from node-mongodb-native
mongoose.Collection.prototype.insert = function(docs, options, callback) {
// this is what the API would do if the save succeeds!
callback(null, docs);
};
module.exports = mongoose;
Generally, as long as models are created after modifying mongoose, it is thinkable that the above mocks are done on per test basis to simulate any behaviour. Make sure to revert to the original behaviour, however, before every test!
Finally this is how our tests for all possible data saving operations could look like. Pay attention, these are not specific to our Post model and could be done for all other models with exactly the same mock in place.
test/test_model.js
// now we have mongoose with the mocked API
// but it is essential that our models are created AFTER
// the API was mocked, not in the main source!
var mongoose = require("./mock"),
assert = require("assert");
var underTest = require("../src");
describe("Post", function() {
var Post;
beforeEach(function(done) {
var conn = mongoose.createConnection();
Post = underTest.model(conn, underTest.PostSchema, "Post");
done();
});
it("given valid data post.save returns saved document", function(done) {
var post = new Post({
title: 'My test post',
postDate: Date.now()
});
post.save(function(err, doc) {
assert.deepEqual(doc, post);
done(err);
});
});
it("given valid data Post.create returns saved documents", function(done) {
var post = new Post({
title: 'My test post',
postDate: 876543
});
var posts = [ post ];
Post.create(posts, function(err, docs) {
try {
assert.equal(1, docs.length);
var doc = docs[0];
assert.equal(post.title, doc.title);
assert.equal(post.date, doc.date);
assert.ok(doc._id);
assert.ok(doc.createdAt);
assert.ok(doc.updatedAt);
} catch (ex) {
err = ex;
}
done(err);
});
});
it("Post.create filters out invalid data", function(done) {
var post = new Post({
foo: 'Some foo string',
postDate: 876543
});
var posts = [ post ];
Post.create(posts, function(err, docs) {
try {
assert.equal(1, docs.length);
var doc = docs[0];
assert.equal(undefined, doc.title);
assert.equal(undefined, doc.foo);
assert.equal(post.date, doc.date);
assert.ok(doc._id);
assert.ok(doc.createdAt);
assert.ok(doc.updatedAt);
} catch (ex) {
err = ex;
}
done(err);
});
});
});
It is essential to note that we are still testing the very low level functionality, but we can use this same approach for testing any business logic that uses Post.create or post.save internally.
The very final bit, let's run the tests:
~/source/web/xxx $ npm test
> xxx#0.1.0 test /Users/osklyar/source/web/xxx
> mocha --recursive
Post
✓ given valid data post.save returns saved document
✓ given valid data Post.create returns saved documents
✓ Post.create filters out invalid data
3 passing (52ms)
I must say, this is no fun to do it that way. But this way it is really pure unit-testing of the business logic without any in-memory or real DBs and fairly generic.
If what you want is test static's and method's of certain Mongoose model, I would recommend you to use sinon and sinon-mongoose. (I guess it's compatible with chai)
This way, you won't need to connect to Mongo DB.
Following your example, suppose you have a static method findLast
//If you are using callbacks
PostSchema.static('findLast', function (n, callback) {
this.find().limit(n).sort('-postDate').exec(callback);
});
//If you are using Promises
PostSchema.static('findLast', function (n) {
this.find().limit(n).sort('-postDate').exec();
});
Then, to test this method
var Post = mongoose.model('Post');
// If you are using callbacks, use yields so your callback will be called
sinon.mock(Post)
.expects('find')
.chain('limit').withArgs(10)
.chain('sort').withArgs('-postDate')
.chain('exec')
.yields(null, 'SUCCESS!');
Post.findLast(10, function (err, res) {
assert(res, 'SUCCESS!');
});
// If you are using Promises, use 'resolves' (using sinon-as-promised npm)
sinon.mock(Post)
.expects('find')
.chain('limit').withArgs(10)
.chain('sort').withArgs('-postDate')
.chain('exec')
.resolves('SUCCESS!');
Post.findLast(10).then(function (res) {
assert(res, 'SUCCESS!');
});
You can find working (and simple) examples on the sinon-mongoose repo.
Related
I am trying to save to two different docs and two different models in the same function, but no matter what I try I seem to get weird errors. It seems like for whatever reason mongoose has made this exclusively not work.
I have two findOne functions nested one is finding the book while the other is finding the prof and the object is to update them both to relate to each other.
Is there a recommended way I should do this perhaps two seperate backend endpoints and two seperate functions? that would be one solution to this problem, but I'd like to know why I cannot do whats below.
await prof.save().then(async () => {
await book
.save()
.then(() => {
return res.status(200).json({
success: true,
message: 'items updated'
}).catch( (err) => {
return res.status(400)
})
})
})
first of all you can not using await and .then .catch together. if you want update two or more collection in mongoose it's better use transactions, it's like rollback in relational database, but if you are newbie in mongoose it's hard to implement transaction,
without transaction you can do like this
try {
await prof.save();
await book.save();
return res.status(200).json({
success: true,
message: "items updated",
});
} catch (error) {
return res.status(400)
}
You probably want to have a look at Mongoose's documentation on middleware, especially the part about pre .save() middleware.
What you want to do is to trigger some function each time an item is saved. Particularly, you want this function to be another save function itself. This needs to be done in your Profs Schema, and you need to require your Books model to make it work:
ProfsSchema.pre('save', async function (next) {
const prof = this;
try {
await Books.findOneAndUpdate({ ... });
next();
} catch (e) {
throw e;
}
})
Of course, this is a very schematic suggestion: the { ... } represents whatever updates you want to apply to your book document. Also, you may want to do different things whether a new prof document is being created with a if (prof.isNew) or only trigger these functions when certain fields have been modified if (prof.field.isModified).
In any case, I strongly suggest you learn to use Mongoose's middleware for this kind of purpose: it's a rather powerful tool to interconnect different models across a MongoDB cluster.
I'm running asynchronous tests with nodeJS/mocha/mongoose and I keep getting duplicate documents in my collection when I run a test like the following.
const assert = require('assert');
const User = require('../src/user');
describe('Duplicates records test', () => {
beforeEach((done) => {
let user = new User({ name: 'Bob'});
user.save()
.then(() => done());
});
it('Return users named Bob', (done) => {
User.find({ name: 'Bob' })
.then((users) => {
console.log(users);
done();
});
});
});
The following is the model I'm using:
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: String
});
const User = mongoose.model('user', UserSchema);
module.exports = User;
The following is the output from running the test:
[ { _id: 5c918ca6d4b6eb4416312226, name: 'Bob', __v: 0 },
{ _id: 5c918ca6c2589d4415a4317a, name: 'Bob', __v: 0 } ]
1 passing (2s)
And finally the output from mongodb:
> db.users.find().pretty()
{ "_id" : ObjectId("5c918ca6d4b6eb4416312226"), "name" : "Bob", "__v" : 0 }
{ "_id" : ObjectId("5c918ca6c2589d4415a4317a"), "name" : "Bob", "__v" : 0 }
I have several tests like this in my suite and this only seems to happen when I save records inside of a beforeEach statement. Any insight as to why this is happening would be greatly appreciated.
This is probably happening because you have no logic to drop the previous record saved when you fire off the next test.
To avoid duplicate records when you are testing with NodeJS/Mocha/mongoose you want to add some logic to drop the collections at least when running your server for testing.
You have not shared your package.json file, but I would recommend a script that looks like this:
"scripts": {
"test": "NODE_ENV=test nodemon --exec 'mocha --recursive -R min'"
},
So NODE_ENV is an environment variable and environment variables are used to customize the behavior or encode some constants to access inside an application and you can use NODE_ENV variable to specify if the server is running in development, production or whatever.
So this will help you in seeing if it needs to empty out the collection of users.
To do so, you can make a reference to the variable process.env.NODE_ENV.
Wherever you are making your connection to Mongo, you want to add an if statement like so:
if (process.env.NODE_ENV !== "test") {
mongoose.connect("mongodb://localhost/user", { useMongoClient: true });
}
So what you are saying here is, when running in development just start up the sever and connect to the database but when running in test, start up the server, connect to the database and drop the collections, but that is not the solution in and of itself.
What I would recommend is creating a separate database that would be called user_test and then create a separate helper file with the logic for connecting to that database.
So you are going to have two separate databases.
So in the if statement above you are saying if I am running in a development or production environment, if its not equal to test, then connect to this database. If I am running in test do not connect to this database.
Then you add that test helper file to the application that will be used to connect to mongoose. Why a separate test helper file? because it will make it easier to ensure the test suite only starts up once the connection to the test database has been achieved.
Keep in mind that Mocha and Mongoose don't really play well together or at least not in my opinion.
This is also helpful because you do not have to wrap the connection string with an if statement to check if you are in the test environment. If the test helper file is executed then yeah you are in the test environment so you don’t have to make it explicit. That logic would look something like this:
const mongoose = require(‘mongoose’);
before((done) => {
mongoose.connect("mongodb://localhost/user_test”, { useMongoClient: true });
});
So now the user_test database can get dropped all day long and nothing bad is going to happen, you won’t be touching the development data and so later you can even customize that development database to have some data that you might want to have stick around.
You also place handlers for looking at the connection status and once the connection is open you can use a done callback like so:
const mongoose = require(‘mongoose’);
before((done) => {
mongoose.connect("mongodb://localhost/user_test”, { useMongoClient: true });
mongoose.connection.once(‘open’, () => done())
});
If there is ever an error you can launch a console.warn() like so:
const mongoose = require(‘mongoose’);
before((done) => {
mongoose.connect("mongodb://localhost/muber_test”, { useMongoClient: true });
mongoose.connection.once(‘open’, () => done()).on(‘error’, (err) => {
console.warn(‘Warning’, error);
});
});
I like this solution because if you locate all the logic inside of the test itself, sometimes Mocha will execute the test before the connection has been made to the database which means all the tests will fail as a result.
Almost forgot the most important part, dropping the collection:
const mongoose = require(‘mongoose’);
before((done) => {
mongoose.connect("mongodb://localhost/muber_test”, { useMongoClient: true });
mongoose.connection.once(‘open’, () => done()).on(‘error’, (err) => {
console.warn(‘Warning’, error);
});
});
beforeEach((done) => {
const { users } = mongoose.connection.collections;
users.drop().then(() => done()).catch(() => done());
});
The reason we have that catch() in there for done() is that it handles the very first time your database runs, you do not have yet a users collection in existence and that would throw an error and that's why we add that to handle the case.
I tried to write a unit test for sails without doing a sails lift. I have two simple controllers, UserController that has a model with an id, name, & schoolid, then a SchoolController with a model that has an id & name using the sails doc: https://sailsjs.com/documentation/concepts/testing. I wrote a simple test with sails lift/sails.lower. I'm able to get the controller waterline action and it works for me, however, if I want to write a simple unit test without lifting sails I get a User is not defined error. How can I simply get a sails waterline action without lifting sails? I found one great library wolfpack https://github.com/fdvj/wolfpack but it doesn't work.
//SchoolController
function checkSchool(req, res) {
const id = req.param('id');
const idquery = ;
userquery(`SELECT * FROM school WHERE id = ${id}`).then((rows) => {
const schoolId = rows[0].school;
User.findOne({ id: schoolId }).exec((school) => {
res.status(200).send(school);
});
});
}
module.exports = {
checkSchool
};
//SchoolController.test.js
let SchoolController = require('../src/controller/SchoolController');
describe('SchoolController', function(){
We can test that our controller is calling our Model method with the proper params
it("should get one school info by user id a given id", function(){
SchoolController.checkSchool(request, response);
console.log(response);
});
});
I am quite new to testing nodejs. So my approach might be completely wrong. I try to test a mongoose models pre-save-hook without hitting the Database. Here is my model:
// models/user.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
UserSchema = new Schema({
email: {type: String, required: true},
password: {type: String, required: true}
});
UserSchema.pre('save', function (next) {
const user = this;
user.email = user.email.toLowerCase();
// for testing purposes
console.log("Pre save hook called");
next();
});
module.exports = mongoose.model("User", UserSchema);
As I said, I do not want to hit the Database with my test, so I tried using a sinon stub of the Users save() method:
// test/models/user.js
const sinon = require("sinon");
const chai = require("chai");
const assert = chai.assert;
const User = require("../../models/user");
describe("User", function(){
it("should convert email to lower case before saving", (done) => {
const user = new User({email: "Valid#Email.com", password: "password123"});
const saveStub = sinon.stub(user, 'save').callsFake(function(cb){ cb(null,this) })
user.save((err,res) => {
if (err) return done(err);
assert.equal(res.email,"valid#email.com");
done();
})
})
});
However, If I do it like that the pre-save hook will not be called. Am I on the wrong path or am I missing something? Or is there maybe another way of triggering the pre-save hook and testing its outcome? Thanks very much in advance!
Before we start: I'm looking for the same thing as you do and I've yet to find a way to test the different hooks in Mongoose without a database. It's important that we distinguish between testing our code and testing mongoose.
Validation is middleware. Mongoose registers validation as a pre('save') hook on every schema by default. http://mongoosejs.com/docs/validation.html
Considering that validate will always be added to the model and I wish to test the automated fields in my model, I've switched from save to validate.
UserSchema = new Schema({
email: {type: String, required: true},
password: {type: String, required: true}
});
UserSchema.pre('validate', function(next) {
const user = this;
user.email = user.email.toLowerCase();
// for testing purposes
console.log("Pre validate hook called");
next();
});
The test will now look like:
it("should convert email to lower case before saving", (done) => {
const user = new User({email: "VALID#EMAIL.COM", password: "password123"});
assert.equal(res.email,"valid#email.com");
}
So What About the Pre Save Hook?
Because I've moved the business logic for automatic fields from 'save' to 'validate', I'll use 'save' for database specific operations. Logging, adding objects to other documents, and so on. And testing this only makes sense with integration with a database.
I just faced the same issue and managed to solve it by extracting the logic out of the hook, making it possible to test it in isolation. With isolation I mean without testing anything Mongoose related.
You can do so by creating a function, that enforces your logic, with the following structure:
function preSaveFunc(next, obj) {
// your logic
next();
}
You can then call it in your hook:
mySchema.pre('save', function (next) { preSaveFunc(next, this); });
This will make the reference to this available inside the function, so you can work with it.
The extracted part can then be unit tested by overwriting the next function to a function without a body.
Hope this will help anyone as it actually was a pain to solve this with my limited knowledge on Mongoose.
I need to retrieve an object and also get the relations and nested relations.
So, I have the three models below:
User model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pets: {
collection: 'pet',
via: 'owner',
}
}
Pet model:
module.exports = {
attributes: {
name: {
type: 'string'
},
owner: {
model: 'user'
},
vaccines: {
collection: 'vaccine',
via: 'pet',
}
}
Vaccine model:
module.exports = {
attributes: {
name: {
type: 'string'
},
pet: {
model: 'pet'
}
}
Calling User.findOne(name: 'everton').populate('pets').exec(....) I get the user and associated Pets. How can I also get the associated vaccines with each pet? I didn't find references about this in the official documentation.
I've ran into this issue as well, and as far as I know, nested association queries are not built into sails yet (as of this post).
You can use promises to handle the nested population for you, but this can get rather hairy if you are populating many levels.
Something like:
User.findOne(name: 'everton')
.populate('pets')
.then(function(user) {
user.pets.forEach(function (pet) {
//load pet's vaccines
});
});
This has been a widely discussed topic on sails.js and there's actually an open pull request that adds the majority of this feature. Check out https://github.com/balderdashy/waterline/pull/1052
While the answer of Kevin Le is correct it can get a little messy, because you're executing async functions inside a loop. Of course it works, but let's say you want to return the user with all pets and vaccines once it's finished - how do you do that?
There are several ways to solve this problem. One is to use the async library which offers a bunch of util functions to work with async code. The library is already included in sails and you can use it globally by default.
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets;
// async.each() will perform a for each loop and execute
// a fallback after the last iteration is finished
async.each(pets, function (pet, cb) {
Vaccine.find({pet: pet.id})
.then(function(vaccines){
// I didn't find a way to reuse the attribute name
pet.connectedVaccines = vaccines;
cb();
})
}, function(){
// this callback will be executed once all vaccines are received
return res.json(user);
});
});
There is an alternative approach solving this issue with bluebird promises, which are also part of sails. It's probably more performant than the previous one, because it fetches all vaccines with just one database request. On the other hand it's harder to read...
User.findOneByName('TestUser')
.populate('pets')
.then(function (user) {
var pets = user.pets,
petsIds = [];
// to avoid looping over the async function
// all pet ids get collected...
pets.forEach(function(pet){
petsIds.push(pet.id);
});
// ... to get all vaccines with one db call
var vaccines = Vaccine.find({pet: petsIds})
.then(function(vaccines){
return vaccines;
});
// with bluebird this array...
return [user, vaccines];
})
//... will be passed here as soon as the vaccines are finished loading
.spread(function(user, vaccines){
// for the same output as before the vaccines get attached to
// the according pet object
user.pets.forEach(function(pet){
// as seen above the attribute name can't get used
// to store the data
pet.connectedVaccines = vaccines.filter(function(vaccine){
return vaccine.pet == pet.id;
});
});
// then the user with all nested data can get returned
return res.json(user);
});