如何在JavaScript全局命名空间中进行函数的存根/模拟?

20

在测试中,我试图对一个写入数据库日志的函数调用进行存根/模拟/覆盖。

function logit(msg) {
  writeMessageToDb(msg);
}

function tryingToTestThisFunction(){
  var error = processSomething();
  if (error) {
    logit(error);
  }
}
我想让logit()在测试期间简单地打印到控制台...但在logit()函数内部添加"isTesting()"的if/else块不是一个选项。
是否可能在不包括其他模拟框架的情况下实现此目的?我目前正在使用JsTestDriver进行单元测试,并没有机会评估任何模拟框架。目前的理想解决方案是在没有另一个框架的情况下处理这个问题。
6个回答

10

我使用Jasmine和Sinon.js(使用Coffeescript),以下是我如何对confirm()方法进行存根,例如只返回true。

beforeEach ->
  @confirmStub = sinon.stub(window, 'confirm')
  @confirmStub.returns(true)

afterEach ->
  @confirmStub.restore()

4
这个案例与众不同,因为Window.confirm()不是一个全局函数。 - Lance Kind
虽然它本质上是全局函数。在控制台中尝试 confirm('test')window 本身就是全局对象(甚至公开了 window.window - 以及全局定义的用户函数)。在 Node 中,global 将是期望的目标(例如,为了存根 setTimeout 而不需要花费太长时间)。或者如果在 Node 12+ 上,不需要 IE 支持或非更新的浏览器版本,则使用 globalThis - Brett Zamir

4
在JavaScript中,最新的定义是占主导地位的。因此,在第一次定义之后,只需重新定义logit方法即可。
function logit(msg) {
  console.log(msg);
}

example : http://www.jsfiddle.net/gaby/UeeQZ/


3

Javascript不仅是在运行时链接的,而且还是最后一个声明胜出的链接。这意味着你可以重新声明具有所需行为的方法在你的测试中(因为你的测试拥有最后一句话):

function yourTest(){
    oldImpl = logit;   // An even better approach is to do this in a setup.
    logit = function(msg){ Console.log.apply(console, s.call(arguments));};
    // do you assertions: assert.... yada.

    logit = oldImpl;  // Do this to keep your test isolated from the others you'll be executing in this test run. An even better approach is to do this in a teardown.
}

1
这是唯一正确的答案。以上答案只会在你想对重载函数进行单元测试时带来混淆,因为它将不具有其原始功能。 - iSpain17

3
我一直在研究同样的问题。开发人员给了我一个HTML5应用程序进行测试,所以我当然不能更改他们的代码来进行测试。我决定使用qunitsinon,以及sinon-qunit。对于像我这样的JavaScript单元测试新手,我被sinon文档和网上的各种示例弄疯了,因为大多数情况下似乎都是针对未提及的隐含环境而编写的。下面的代码是一个完整的页面,所以我希望没有任何混淆的地方。
我必须调用的函数是caller(),而我无法对stubme()进行任何操作,因为它在开发人员的代码中。但是,我可以在我的测试代码中添加sinonstub()。但如何让它与sinon一起工作呢?sinon文档让我困惑了一段时间,但下面是简单的解决方案。stub4stubme对象可用于控制存根操作,并获取有关存根调用正在发生的信息。
<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
    <link rel="stylesheet" href="qunit-1.12.0.css" type="text/css" media="screen" />
</head>
<body>
    <div id="qunit"></div>
    <div id="qunit-fixture"></div>
    <script src="sinon-1.7.3.js"></script>
    <script src="qunit-1.12.0.js"></script>
    <script src="sinon-qunit-0.8.0.js"></script>
    <script>

        // Dev code in another file
        function stubme() {
            return "stubme";
        }

        function caller() {
            return "caller " + stubme();
        }
        // End of dev code

        var sinonstub = function () {
            return "u haz bin stubbed";
        };

        test("Stubbing global environments", function () {
            equal(caller(), "caller stubme");

            var stub4stubme = this.stub(window, "stubme", sinonstub);

            equal(caller(), "caller u haz bin stubbed");
            ok(stubme.called);
        });

    </script>
</body>
</html>

你几乎已经理解了我的困惑。Sinon.stub(window, "stubme", sinonstub); 为什么"window"是顶层对象名称?这里涉及到全局变量环境的问题,在你的例子中,它被称为window,因为它在窗口中运行。但原始帖子可能在Node.js中,那么它还是"window"吗? - Lance Kind

1

你能否直接覆盖窗口对象上的方法?在Chrome控制台中这样做是可行的。

function test() {console.log('test')};
window.test();

1

只需覆盖logit函数,这可以在定义logit之后的任何时候调用。

(function(){
  //keep handle to original logit method.
  var ol = logit;

  //shorter lookup path for slice
  var s = Array.prototype.slice;

  //logit override
  logit = function() {
    //if in testing
    if (typeof IsTesting == "function" && !!IsTesting()) {
      //log the arguments
      console.log.apply(console, s.call(arguments));
    } else {
      //otherwise, call the original function.
      ol.apply(this, s.call(arguments))
    }
  }
}());

请注意:您可以将“msg”作为参数直接传递,使用方法上的s.call(arguments)和.apply将一切传递,这对于此类方法覆盖更有效。 - Tracker1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接