1

I have a function, which computes some stuff, notifying the user via callbacks about some events:

function returnAndCallback(callback) {
  callback(5);  // not always invoked
  return 3;
}

Using Mocha and Should.js, I wrote this test:

describe('mixing sync and async code', function() {
  it('should test callback AND return value', function(done) {
    returnAndCallback(function(value) {
      value.should.be.eql(5);
      done();
    }).should.be.eql(4);
  });
});

This succeeds because the test ends when done() is called. It seems, I can either write a synchronous test and check the return value, or I can write an asynchronous test and check the callback.

One can not use a sync test like this:

describe('mixing sync and async code', function() {
  it('should test callback AND return value', function() {
    returnAndCallback(function(value) {
      should.be.eql('difficult to get result');
    }).should.be.eql(3);
  });
});

... because I want to assert that the callback is called. This test would succeed, if the callback is never called.

How can I test both that the callback is called with the right value AND the correct value is returned?

Duplicating tests is the only option I see.

Edit: I notice, that I use the term asynchron wrongly here. The callbacks are merely a way to inject optional actions into a function, which transforms an object. All code is synchronous, but the control flow sometimes branches of into callbacks and I want to be able to recognize that.

Here is another possible way to test that:

describe('mixing sync and async code', function() {
  it('should test callback AND return value', function() {
    let callbackValue;

    returnAndCallback(function(value) {
      callbackValue = value;
    }).should.be.eql(3);

    callbackValue.should.be.eql(5);
  });
});

But still not perfectly elegant due to the extra variable.

Hannes
  • 1,118
  • 8
  • 15
  • 1
    I don't mean to sound pedantic, but your function seems to be wrong. If you have a callback that's what you return. Why would you have sometimes a callback and other times a return? – yBrodsky Jan 30 '17 at 19:06
  • The concrete use case is a validator/checker which gets an object, checks it and maybe changes some fields. One can give a callback, which reports all changes. Finally the checked object is returned. – Hannes Jan 30 '17 at 19:42
  • And in the practise, how do you use such a function? – yBrodsky Jan 30 '17 at 19:45
  • I use Socket.IO and get event message objects from a large number of different client implementations. I want to be as robust as possible in handling those messages. Some are just missing a field which I could fill with a default value, but I still want to notify the client (for debugging), so I send a warn event - which would be emitted in the callback. If something is really wrong, the client would get an error event (which would helped me to spot a typo much faster when I was wondering why the gps position was messed up. I was giving a latutide, but Chrome was silently failing). – Hannes Jan 30 '17 at 19:51
  • Equivalently I could return an object `{ result: ..., warns: [ ... ], errors: [ ... ] }`, but it seemed not as elegant, if I just can do `msg.gps = validate(msg.gps, reportError, reportWarn)`, where the `report*` functions are optional or optionally no-ops. – Hannes Jan 30 '17 at 19:53
  • Well, check @brandonscript answer. He is right. Here you have a simple example: https://jsfiddle.net/7aftvyws/ – yBrodsky Jan 30 '17 at 19:57

2 Answers2

2

First, be aware that done() implies a synchronous test; Mocha's default is to run tests asynchronously. If you want to test the 'returned' value from asynchronous functions (functions that return a value in a callback function), you run them synchronously, via done().

Next, you can't return a value from an asynchronous function. These two behaviours are mutually exclusive:

function returnAndCallback(callback) {
  callback(5);  // not always invoked
  return 3;
}

You want to only execute the callback.

It appears to me that you're expecting that sometimes a callback is passed, but not always. In that case, I'd separate the function tests (I think you'll need to use done() everywhere, to persist the synchronous nature of the tests) and do a check for callback inside the function itself.

Now that we've got that clarified, since you want to assert that a callback is called, we need to establish some baseline assumptions:

  • A) A callback should be a function
  • B) A callback should be called with a parameter that contains a value

You want to test for both of these things. A) is easy to prove: you're writing the callback function as part of your test, so if you passed say, null or undefined, of course the test will fail, but that's not the point of this test. Here is how you prove both A) and B):

function optionalAsync(callback) {
  if (typeof callback === 'function') {
    callback(4)
  } else {
    return 3
  }
}

describe('optional async function', function() {
  it('should return 4 when a callback is passed', function(done) {
    optionalAsync(function(value) {
        should(value).be.eql(4)
        done()
    })
  })
  it('should return 3 when no callback is passed', function(done) {
    optionalAsync().should.be.eql(3)
    done()
  })
})

This is kind of strange, but given your use case, it does make sense to check for both possibilities. I'm sure you could reduce the code footprint a bit too, but I'd suggest keeping it this way for the sake of readability for when you shelve tis for a year and forget what you did ;)

Now after all of this if you still want to have the option for a function to run synchronously you can do so by blocking the event loop: https://stackoverflow.com/a/22334347/1214800.

But why would you want to?

Save yourself the trouble of handling synchronous operations in an inherently non-blocking, asynchronous platform, and write everything (even the non-IO-blocking operations) with a callback:

function optionallyLongRunningOp(obj, callback) {
  if (typeof callback === 'function') {
    validateObject(obj, function(err, result) {
       // not always long-running; may invoke the long-running branch of your control-flow
       callback(err, result)
    })
  } else {
    throw new Error("You didn't pass a callback function!")
  }
}

describe('possibly long-running op async function', function() {
  it('should return 4 when control flow A is entered', function(done) {
    obj.useControlFlow = "A"
    validateObject(obj, function(err, result) {
        // this is a slow return
        should(result.value).be.eql(4)
        done()
    })
  it('should return 3 when control flow B is entered', function(done) {
    obj.useControlFlow = "B"
    validateObject(obj, function(err, result) {
        // this is a quick return
        should(result.value).be.eql(3)
        done()
    })
  })
})

Here is your answer written with everything as callback (even the short ops):

var doLongRunnignOp = function(cb) {
    var didNotify = true
    cb(didNotify)
}

function doubleAndNotifyEven(num, cb) {
  if (num % 2 == 0) {
    doLongRunnignOp(function(didNotify) {
      cb(num)
      // did notify
    })
  } else {
    cb(2 * num)
    // instant return, did not notify
  }
}

describe('checking return value and callback execution', function() {
  it('should double and report given an even number', function() {
    doubleAndNotifyEven(2, function(value) {
      should(value).be.eql(2)
    })
  })

  it('should double and not report anything given an odd number', function() {
    doubleAndNotifyEven(3, function(value) {
      should(value).be.eql(6)
    })
  })
})
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • Thank you very much for your effort! It is not quite what I was looking for. I guess I stated the problem incorrectly (really messed up sync and async). You are right with the handling of async returns and I accept the answer for now. I edited the question and could rephrase it as "How do I make that variable go away?"... – Hannes Jan 30 '17 at 20:08
  • Don't do `value.should.be.eql(4)`, if your `value` is something like `null` or _undefined_ the whole test breaks. Also to get full error feedback, `try {expect(value).to.equal(4);done();} catch (e) {done(e);}` – Paul S. Jan 30 '17 at 20:13
  • Well, yeah, but since the function is explicitly setting the value (it's impossible for it to be null or undefined) I left that stability checking out ;) – brandonscript Jan 30 '17 at 20:16
  • @brandonscript in your update you don't name a param _done_ ;) and still the error outcomes can go missing and you will just see the timeout unless you `done(e); // e some error` – Paul S. Jan 30 '17 at 20:18
  • Hmm, not an optional callback, but an optional callback invocation. Like for `function double(num, reportEven) { if (num % 2 == 0) reportEven(num); return num * 2; }` and I want to have 2 tests with an odd and an even number. In the odd case the callback would throw an Error via `done`, in the even case it would assert the correct parameter of the callback. I tried to make the problem statement as simple as possible, but I probably left out too much. Sorry, still new to stackoverflow... – Hannes Jan 30 '17 at 20:27
  • No matter how you work it, if you return from a function, it'll abort before the callback executes _some of the time_, because you're breaking the Node event chain, and then it's up to chance (CPU cycles) to determine whether the callback will execute in time for the test to catch it. You can't have both unless you explicitly know what to expect — you have to check for existence of the callback function first before you handle it. – brandonscript Jan 30 '17 at 20:32
  • @brandonscript Yes, that is exactly the point where I stated the problem wrongly. It is really not async (no sleeps, IO etc. where the event loop schedules something different), see the edit of the question for that. Just synchronous code with the chance that the library code executes some user supplied functions. And I want to check 1. if the functions are executed exactly *when* they should and 2. if they are executed with the *correct parameters*. Testing 2. is easy and I tried to test 1. by using `done()`, which is wrong, as you summarized pretty well. – Hannes Jan 30 '17 at 20:44
  • Even so, I don't see how you can test for the existence of _both_ possibilities at the same time, expecting the outcome to be consistent. It's possible that it could be done using a hack of `bind()` to bind the function and its optional callback to a sync function, but I can't wrap my head around how to do that right now. – brandonscript Jan 30 '17 at 20:55
  • 1
    Point of discussion, I question why you're writing anything synchronously in the first place? It sort of breaks the entire Node model. You could write everything asynchronously, expecting there to _always_ be a callback, and then simply test what the value from the callback is. – brandonscript Jan 30 '17 at 20:56
  • Well, there are probably a number of reasons. 1. I am new to node and not completely familiar with the async concept. All of my validators are just a few lines without any blocking IO, so I see no reason to go async. 2. I use a kind of plugin system to validate many aspects of a message object. I would nest pretty deep or at least the control flow would be hard to follow, if I use a callback for each return value. It's almost like `for(let key in msg) { msg[key] = validators[key](msg[key], reportError, reportWarn); if (msg[key] === undefined) return; } return msg;` + a bit nested. – Hannes Jan 30 '17 at 21:32
  • 1
    Build your validator into a module and export it - you can leverage functional programming design to map deep object property checking inside the module, then return a simple true/false in a callback. In Node, when in doubt, ALWAYS write asynchronously unless you have to go sync. You lose nothing by writing a function asynchronously (with a callback) even for non-IO-blocking operations. – brandonscript Jan 30 '17 at 21:36
  • Thank you for the suggestion and again for your valuable tips! I will try that tomorrow and see, how everything can be plugged together. – Hannes Jan 30 '17 at 21:43
1

Here is another solution for this problem. It just adds an additional line of code in tests which want to ensure callback execution.

let should = require('should');

function doubleAndNotifyEven(num, reportEven) {
  if (num % 2 == 0) {
    reportEven(num);
  }

  return 2 * num;
}

function mandatoryCallback(callback) {
  mandatoryCallback.numCalled = 0;
  return function () {
    mandatoryCallback.numCalled++;
    return callback.apply(this, arguments);
  };
}

describe('checking return value and callback execution', function() {
  it('should double and report given an even number', function() {
    doubleAndNotifyEven(2, mandatoryCallback(function(value) {
      should(value).be.eql(2);
    })).should.be.eql(4);

    should(mandatoryCallback.numCalled).greaterThan(0);
  });

  it('should double and not report anything given an odd number', function() {
    doubleAndNotifyEven(3, function(value) {
      throw new Error('Wrong report!');
    }).should.be.eql(6);
  });
});

Please also note sinon which does something similar.

Hannes
  • 1,118
  • 8
  • 15
  • Where this may fail is that it relies on your num check in `doubleAndNotifyEven()` to evaluate true; this means you're still relying on specific conditions to determine whether the callback should fire. – brandonscript Jan 30 '17 at 21:56