Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing mongoose pre-save hook

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: "[email protected]", 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,"[email protected]");
            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!

like image 485
Martin Reiche Avatar asked Sep 22 '17 17:09

Martin Reiche


2 Answers

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: "[email protected]", password: "password123"});
    assert.equal(res.email,"[email protected]");
}

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.

like image 171
R. Gulbrandsen Avatar answered Sep 21 '22 16:09

R. Gulbrandsen


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.

like image 36
SomeDutchGuy Avatar answered Sep 17 '22 16:09

SomeDutchGuy