【问题标题】:Mixing sync and async tests using Mocha使用 Mocha 混合同步和异步测试
【发布时间】:2017-01-30 18:42:31
【问题描述】:

我有一个函数,它计算一些东西,通过回调通知用户一些事件:

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

使用 Mocha 和 Should.js,我编写了这个测试:

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);
  });
});

这会成功,因为测试在调用done() 时结束。看来,我可以编写一个同步测试并检查返回值,也可以编写一个异步测试并检查回调。

不能像这样使用同步测试:

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);
  });
});

... 因为我想断言回调被调用。如果从不调用回调,则此测试将成功。

如何测试回调是否使用正确的值和返回正确的值?

复制测试是我看到的唯一选择。

编辑:我注意到,我在这里错误地使用了异步一词。回调只是将可选操作注入到函数中的一种方式,该函数可以转换对象。所有代码都是同步的,但控制流有时会分支到回调中,我希望能够识别这一点。

这是另一种可能的测试方法:

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);
  });
});

但由于额外的变量,仍然不够优雅。

【问题讨论】:

  • 我并不是要听起来很迂腐,但您的功能似乎是错误的。如果你有一个回调,那就是你返回的。为什么有时会有回调而有时会返回?
  • 具体用例是一个验证器/检查器,它获取一个对象,检查它并可能更改一些字段。可以给一个回调,它报告所有的变化。最后返回选中的对象。
  • 而在实践中,你如何使用这样的功能?
  • 我使用 Socket.IO 并从大量不同的客户端实现中获取事件消息对象。我希望在处理这些消息时尽可能强大。有些只是缺少一个我可以用默认值填充的字段,但我仍然想通知客户端(用于调试),所以我发送了一个警告事件 - 这将在回调中发出。如果确实有问题,客户端会收到一个错误事件(当我想知道为什么 gps 位置搞砸时,这将帮助我更快地发现错字。我给出了一个 latutide,但 Chrome 默默地失败了)。
  • 等效地,我可以返回一个对象 { result: ..., warns: [ ... ], errors: [ ... ] },但如果我只能执行 msg.gps = validate(msg.gps, reportError, reportWarn),它似乎并不那么优雅,其中 report* 函数是可选的或可选的无操作。跨度>

标签: javascript node.js unit-testing mocha.js


【解决方案1】:

首先,请注意done() 意味着同步测试; Mocha 的默认设置是异步运行测试。如果您想测试 异步 函数(在回调函数中返回值的函数)的“返回”值,请通过done() 同步运行它们。

接下来,您不能从异步函数返回值。这两种行为是互斥的:

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

您只想执行回调。

在我看来,您希望有时会传递一个回调,但并非总是如此。在这种情况下,我会分离函数测试(我认为您需要在任何地方使用done(),以保持测试的同步性质)并检查函数本身内部的回调。

既然我们已经弄清楚了,既然你想断言回调被调用,我们需要建立一些基线假设:

  • A)回调应该是一个函数
  • B) 应使用包含值的参数调用回调

你想测试这两个东西。 A) 很容易证明:你正在编写回调函数作为测试的一部分,所以如果你通过了null 或undefined,当然测试会失败,但这不是这个测试的重点。下面是你如何证明 A) 和 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()
  })
})

这有点奇怪,但考虑到您的用例,检查这两种可能性确实有意义。我相信你也可以稍微减少代码占用空间,但我建议保持这种方式,以便在你搁置一年而忘记你所做的事情时保持可读性;)

现在,如果您仍然希望选择一个函数同步运行,您可以通过阻塞事件循环来做到这一点:https://stackoverflow.com/a/22334347/1214800。

但是你为什么想要呢?

省去在固有的非阻塞、异步平台中处理同步操作的麻烦,并使用回调编写所有内容(甚至是非 IO 阻塞操作):

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()
    })
  })
})

这是您的答案,所有内容都作为回调(甚至是短操作):

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)
    })
  })
})

【讨论】:

  • 非常感谢您的努力!这不是我想要的。我想我错误地陈述了这个问题(真的搞砸了同步和异步)。您对异步返回的处理是正确的,我现在接受答案。我编辑了这个问题,并可以将其改写为“如何使该变量消失?”...
  • 不要使用value.should.be.eql(4),如果您的value 类似于null 或undefined,整个测试就会中断。还要获得完整的错误反馈,try {expect(value).to.equal(4);done();} catch (e) {done(e);}
  • 嗯,是的,但是由于该函数明确设置了值(它不可能为 null 或未定义),所以我离开了稳定性检查;)
  • 讨论点,我质疑你为什么要同步写任何东西?它有点破坏了整个 Node 模型。您可以异步编写所有内容,期望那里总是是一个回调,然后简单地测试来自回调的值是什么。
  • 将您的验证器构建到模块中并将其导出 - 您可以利用函数式编程设计来映射模块内的深层对象属性检查,然后在回调中返回简单的真/假。在 Node 中,如果有疑问,除非您必须同步,否则总是异步写入。即使对于非 IO 阻塞操作,异步编写函数(使用回调)也不会造成任何损失。
【解决方案2】:

这是解决此问题的另一种方法。它只是在要确保回调执行的测试中添加了额外的代码行。

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);
  });
});

还请注意sinon 做了类似的事情。

【讨论】:

  • 这可能失败的地方是它依赖于您在doubleAndNotifyEven() 中的 num 检查来评估 true;这意味着您仍然依赖特定条件来确定回调是否应该触发。
猜你喜欢
  • 2017-06-20
  • 1970-01-01
  • 1970-01-01
  • 2012-08-22
  • 2016-04-05
  • 1970-01-01
  • 2013-11-29
  • 2014-02-08
  • 2020-03-26
相关资源
最近更新 更多