How to test if a method is being called in the controller?

559 views Asked by At

This is a question regarding asynchronoust testing in Sails JS using Mocha.

I am writing controller test in Sails JS using supertest library. I want to check if a method is being called on HTTP POST to our controller. For that, I am stubbing the method and expecting it to be invoked in the end() as follows:

request(sails.hooks.http.app)
    .post('heartbeat/create')
    .send('device: 1')
    .end(function(err, res) {
        expect(publishCreateStub.called).to.be.true;
        done();
    });

When I run this, the expectation fails because the method is not called when asserting. But when I put the expectation in a setTimeout as follows, it works:

request(sails.hooks.http.app)
    .post('heartbeat/create')
    .send('device: 1')
    .end(function(err, res) {
        setTimeOut(function() {
            expect(publishCreateStub.called).to.be.true;
            done();
        }, 1000);
    });

Is there any way to make the test pass without a setTimeout?

Here is the code part I am testing: HeartbeatController#create

You can also help us to resolve the issue by sending pull requests: https://github.com/multunus/one-mdm/issues/1

2

There are 2 answers

0
Leonid Beschastny On BEST ANSWER

The actual problem is that your controller don't wait for publishCreate to be called. So, you're responding with 201 Created without checking that anything was created.

There is a chance that either Heartbeat.findOne or Heartbeat.publishCreate will fail, but you'll newer know it.

To fix this problem you should modify your controller, moving response part inside Heartbeat promise callback:

create: function (req, res, next) {
  if(req.isSocket) {
    Heartbeat.watch(req);
  }

  Heartbeat.create(req.body)
    .exec(function(error, heartbeat) {
      if(error) {
        res.status(422);
        return res.send('Invalid heartbeat data');
      }

      Heartbeat.findOne(heartbeat.id).populate('device').then(function(newHeartbeat) {
        Heartbeat.publishCreate(newHeartbeat.device);
      }).then(function() {
        // success
        res.status(201);
        res.json({
          device: heartbeat.device
        });
      }, function(err) {
        // something bad happened
        next(err);
       });

    });
}

In my example I'm delegating an actual error handling to the next express error-handling middleware:

next(err);

But you may decide to handle the error yourself instead, e.g.:

res.status(400);
res.send('Can not publish Heartbeat create');
0
leftclickben On

Your problem is not in your test, it's in your application:

Heartbeat.create(req.body)
  .exec(function(error, heartbeat) {
    if(error) {
      res.status(422);
      return res.send('Invalid heartbeat data');
    }        

    Heartbeat.findOne(heartbeat.id).populate('device').then(function(newHeartbeat) {
      Heartbeat.publishCreate(newHeartbeat.device);
    });

    res.status(201);
    return res.json({
      device: heartbeat.device
    });
  });
}

https://github.com/multunus/one-mdm/blob/master/api/controllers/HeartbeatController.js

Here you are creating a promise but not returning that promise. So you have no way of waiting for the promise to be fulfilled (or error) before continuing.

Without knowing your application in more detail, I'm not sure of the fix. I would look at abstracting the bit in the middle into a service*, then your HeartbeatController test can just check if the service is called (synchronously) and your test for the service can do the async part that uses the promise.

* Update: by "service", I just mean abstract it away from any concerns of HTTP, so it doesn't know about the request or response objects. The end result is that you've disentangled the HTTP part (the controller) from the part that manages the Heartbeat objects. This makes both parts easier to unit test.