2

I'm using a save middleware in Mongoose to create a log of activity in the DB whenever some action is taken. Something like

UserSchema.post("save", function (doc) {
    mongoose.model("Activity").create({activity: "User created: " + doc._id});
});

This appears to work fine, but the problem is that I can't test it because there is no way to pass a callback to post (which probably would not make sense). I test this out using mocha with:

User.create({name: "foo"}, function (err, user) {
    Activity.find().exec(function (err, act) {
        act[0].activity.should.match(new RegExp(user._id));
        done();
    });
});

The problem is that the Activity.create apparently does not finish before .find is called. I can get around this by wrapping .find in setTimeout, but this seems hacky to me. Is there any way to test asynchronous mongoose middleware operations?

Explosion Pills
  • 188,624
  • 52
  • 326
  • 405

2 Answers2

1

Unfortunately, there's not a way to reliably interleave these two asynchronous functions in the way you'd like (as there aren't threads, you can't "pause" execution). They can complete in an inconsistent order, which leaves you to solutions like a timeout.

I'd suggest you wire up an event handler to the Activity class so that when an Activity is written/fails, it looks at a list of queued (hashed?) Activities that should be logged. So, when an activity is created, add to list ("onactivitycreated"). Then, it will eventually be written ("onactivitywritten"), compare and remove successes maybe (not sure what makes sense with mocha). When your tests are complete you could see if the list is empty.

You can use util.inherits(Activity, EventEmitter) for example to extend the Activity class with event functionality.

Now, you'll still need to wait/timeout on the list, if there were failures that weren't handled through events, you'd need to handle that too.

Edit -- Ignore the suggestion below as an interesting demo of async that won't work for you. :)

If you'd like to test them, I'd have a look at a library like async where you can execute your code in a series (or waterfall in this case) so that you can first create a User, and then, once it completes, verify that the correct Activity has been recorded. I've used waterfall here so that values can be passed from one task to the next.

async.waterfall([
    function(done) {
       User.create({name: "foo"}, function (err, user) {
           if (err) { done(err); return; }
           done(null, user._id);  // 2nd param sent to next task as 1st param
       });

    },
    function(id, done) {  // got the _id from above
        // substitute efficient method for finding
        // the corresponding activity document (maybe it's another field?)
        Activity.findById(id, function (err, act) {
            if (err) { done(err); return; }
            if (act) { done(null, true);
            done(null, false); // not found?!
        });
    }
], function(err, result) {
   console.log("Success? " + result);
});
WiredPrairie
  • 58,954
  • 17
  • 116
  • 143
  • `waterfall` would have the same problem since the middleware operation may not complete even though `User.create` does (and calls `done`). So I guess the answer is using `setTimeout` :/ – Explosion Pills Aug 28 '13 at 19:38
  • Are you saying the call to Create fails completely (doesn't return at all and doesn't call the callback)? – WiredPrairie Aug 28 '13 at 19:43
  • No, `create` works fine, but the in your example `done` will be called before the `Activity.create` finishes so `.find` will not find anything. – Explosion Pills Aug 28 '13 at 19:44
  • Ahh. Gotcha. Alternate suggestion provided using events. The setTimeout seems fragile, so I wanted something that avoided it. :) – WiredPrairie Aug 28 '13 at 20:02
  • Your suggestion is interesting, but I don't have access to the `Activity` instance to use `.on` with. Is there any way to make the events globally accessible or statically accessible on the class? – Explosion Pills Aug 28 '13 at 20:09
  • You could just create a global `ActivityTracker` class and instance to have the events and tracking. You might even be able to use middleware to wire it up. :) There wouldn't really be a decent way to make it static as anything but an illusion. – WiredPrairie Aug 28 '13 at 20:28
0

Async post-save middleware will apparently be available in Mongoose 4.0.0:

https://github.com/LearnBoost/mongoose/issues/787

https://github.com/LearnBoost/mongoose/issues/2124

For now, you can work around this by monkey-patching the save method on the document so that it supports async post-save middleware. The following code is working for me in a similar scenario to yours.

// put any functions here that you would like to run post-save
var postSave = [
  function(next) {
    console.log(this._id);
    return next();
  }
];

var Model = require('mongoose/lib/model');

// monkey patch the save method
FooSchema.methods.save = function(done) {
  return Model.prototype.save.call(this, function(err, result) {
    if (err) return done(err, result);

    // bind the postSave functions to the saved model
    var fns = postSave.map(function(f) {return f.bind(result);});
    return async.series(fns,
      function(err) {done(err, result);}
    );

  });
};
Brandon
  • 917
  • 10
  • 11