测试Javascript函数的阻塞行为

时间:2012-09-28 14:11:45

标签: javascript testing

在Javascript中,我有两个版本的递归函数,一个是同步运行的,另一个是使用简单的调度来异步运行。给定某些输入,在这两种情况下,该函数都应具有无限的执行路径。我需要为这些函数开发测试,特别是检查异步版本是否阻止主线程的测试。

我已经有测试在非返回情况下检查这些函数的输出回调行为,我只关心测试阻塞行为。我可以限制函数运行多长时间但是有限的时间用于测试目的。我目前正在使用QUnit,但可以切换到另一个测试框架。

如何测试非返回的异步函数是否阻止?

编辑,澄清

这将是我正在使用的功能的一个简单例子:

function a()
{
    console.log("invoked");
    setTimeout(a, 1000);
}

a();

我故意在我的描述中滥用某些线程术语,因为我觉得他们最清楚地表达了这个问题。通过不阻塞主线程,我的意思是调用该函数不会阻止其他逻辑的调度和执行。我希望函数本身将在主线程上执行,但我认为函数在运行,只要它计划在将来执行。

7 个答案:

答案 0 :(得分:2)

单元测试基于单一责任原则和隔离(将测试对象与其依赖性分开)。

在这种情况下,你希望你的函数是异步运行的,但是这个行为不是由你的函数完成的,是由“setTimeout”函数完成的,所以我认为你必须将你的函数与“setTimeout”隔离开,因为它是一个依赖你不想测试,浏览器保证它可以工作。

然后,因为我们相信“setTimeout”将执行asyncrhonous逻辑,我们只能测试我们对“setTimeout”的函数调用,我们可以用另一个函数替换“window.setTimeout”,而我们必须始终将其恢复测试完成。

function replaceSetTimeout() {
    var originalSetTimeout = window.setTimeout;
    var callCount = 0;

    window.setTimeout = function() {
        callCount++;
    };

    window.setTimeout.restore = function() {
        window.setTimeout = originalSetTimeout;
    };

    window.setTimeout.getCallCount = function() {
        return callCount;
    };
}

replaceSetTimeout();
asyncFunction();
assert(setTimeout.getCallCount() === 1);
setTimeout.restore();

我建议您使用sinon.js,因为它提供了许多工具,例如spies,这些工具的功能比通知您多少次以及调用哪些参数。

var originalSetTimeout = window.setTimeout;
window.setTimeout = sinon.spy();
asyncFunction();

// check called only once
assert(setTimeout.calledOnce);
// check the first argument was asyncFunction
assert(setTimeout.calledWith(asyncFunction));

Sinon还提供fake timers进行setTimeout替换,但具有更多功能,如.tick(x)方法将模拟“x”毫秒但在这种情况下我认为它对你没有帮助

更新以回答问题编辑:

1 - 您的函数无限执行,因此您无法在不中断执行的情况下对其进行测试,因此您必须在某处覆盖“setTimeout”。

2 - 您希望您的函数以递归方式执行,允许在迭代之间执行其他代码吗?大!但理解比你的函数不能这样做你的函数只能调用setTimeout或setInterval并希望这个函数按预期工作。你应该测试你的功能。

3 - 您希望从Javascript(沙盒环境)进行测试而不是另一个Javascript代码使用并释放唯一的一个执行线程(您用来测试)。你真的认为这是一个简单的测试吗?

4 - 但最重要的一个 - 我不喜欢白盒子,因为它将测试与依赖性结合在一起,如果你改变你的依赖关系或将来如何调用它你将不得不改变测试。 DOM函数不存在这个问题,DOM函数会保持相同的界面多年,而现在,除了调用这两个函数之外,你没有其他方法可以做你想做的事情,所以我不认为这个案例“白盒测试”是一个坏主意。

我告诉过你,因为我测试Promise模式实现时遇到了同样的问题而不是总是异步,即使承诺已经完成,我也使用测试引擎异步测试方式测试了它(使用回调函数)并且它是一团糟,测试随机失败,测试执行速度太慢。然后我问TDD专家如何能够如此努力地进行测试,并且他回答说我没有遵循单一责任原则,因为我试图测试我的promise实现和setTimeout行为。

答案 1 :(得分:1)

如果从行为驱动的测试角度考虑,那么'我的功能是否阻止?'不是一个有用的问题。它肯定会阻止,一个更好的问题可能是“它的返回时间不超过50毫秒”。

您可以使用以下内容执行此操作:

test( "speed test", function() {
  var start = new Date();
  a();
  ok(new Date() - start < 50, "Passed!" );
});

这个问题是,如果某人确实做了一些愚蠢的事情,使你的功能无限期阻塞,那么测试就不会失败,它会挂起。

因为JavaScript是单线程的,所以无法解决这个问题。如果我过来并将您的职能改为:

function a() {
    while(true) {
        console.log("invoked")
    }
}

测试将暂停。

通过稍微重构一下,你可以通过这种方式破坏事物。有两件事要做。你的大部分工作和日程安排。将它们分开,您将得到类似以下功能的内容:

function a() {
    // doWork
    var stopRunning = true;

    return stopRunning;
}

function doAsync(workFunc, scheduleFunc, timeout) {
    if (!workFunc()) {
       scheduleFunc(doAsync, [workFunc, scheduleFunc, timeout], timeout);
    }
}

function schedule(func, args, timeout) {
    setTimeout(function() {func.apply(window, args);}, timeout);
}

现在,您可以自由地单独测试所有内容。您可以为doAsync测试提供模拟workFunc和scheduleFunc,以验证它是否按预期运行,您可以测试函数a()而不必担心它是如何安排的。

傻瓜程序员仍然可以在函数a()中加入无限循环,但由于他们不必考虑如何运行更多的工作单元,因此不太可能。

答案 2 :(得分:1)

测试或证明无限执行的执行路径永远不会被阻止,所以你必须将你的问题分成几部分。

您的路径基本上是foo(foo(foo(foo(...etc...)))),从不明白SetTimeout实际上删除了递归。所以你要做的就是测试或证明你的foo没有阻止(我现在告诉你,测试将比证明更“容易”,更多信息如下)

那么,函数foo会阻塞吗?

说一点数学,如果你想知道f(f(...f(x)...))是否总是有值,你实际上只需要证明f(x)总是有x的值f foo 1}}可以返回。如果你能确保它们的返回值很好,那么你有多少递归并不重要。

这对您的foo意味着您只需要证明function(){}不会阻止任何可能的输入值。请记住,在这种情况下,所有全局变量和闭包也是输入值。这意味着您必须对每次通话时使用的每个值进行完整性检查。

要测试,当然你必须替换SetTimeout,但这很简单,如果你用空函数(function foo(n, setTimeout) { var x = global_var; // sanity check n here function f() { setTimeout(f, n) } return f(); } )替换它,很容易证明这个函数不会阻塞或改变你的执行。然后你会

让事情变得更轻松

接受我上面写的内容,这也意味着你必须确保你所使用的全局函数或变量都不会被改变到你的函数破坏到它破坏的点。这实际上非常困难,但您仍然可以通过确保始终使用相同的函数和值来使事情变得更容易,并且其他函数不能通过使用闭包来触及它们。

Math.Pi
  • 这样,您只需在第一次执行时测试这些值。很高兴能够假设"noodles"实际上是Pi而不是包含setTimeout的字符串值。真的很好。
  • 不要使用全局可变对象
  • 使用function() { var x = 0; setTimeout(function(){x = insecure();}, 1); } 调用那些无法规避的内容,以确保无法阻止

    • 如果您需要返回值,事情会变得非常棘手,但可能会考虑到这一点:

      {{1}}

      你所要做的就是

      • 使用x next nextration
      • x的完整性检查值!
  • SetTimeout会阻止吗?

    当然这取决于setTimeout是否阻止。这很难证明,但更容易测试。你实际上无法证明它,因为它的实现取决于解释器。

    我个人认为,当setTimeout的返回值被丢弃时,它的行为就像一个空函数。

答案 3 :(得分:0)

基本上,JavaScript是单线程的,因此阻止主线程。但是:

  • 我假设您正在使用setTimesout来安排您的功能,因此如果对该功能的调用不需要花费太多时间(例如,少于200次),用户将无法注意到或300ms)。

  • 如果你在该功能(包括Canvas或WebGL)中进行DOM操作,那么你就搞砸了。但如果没有,你可以查看 Web Workers ,它可以产生单独的线程,保证不会阻止UI。

但无论如何,JavaScript和主循环,这是一个棘手的问题,过去几个月一直困扰着我,所以你并不孤单!

答案 4 :(得分:0)

在QUnit中实际执行此异步测试实际上是可行的,但在另一个JavaScript测试框架Jasmine JS中处理得更好。我将在两者中提供示例。

在QUnit中,您需要首先调用stop()函数来表示测试预计将异步运行,然后您应该使用包含您的期望的函数调用setTimeout以及调用start()函数完成块。这是一个例子:

test( "a test", function() {
    stop();
    asyncOp();
    setTimeout(function() {
        equals( asyncOp.result, "someExpectedValue" );
        start();
    }, 150 );
});

编辑:显然,您还可以使用整个asyncTest构造来简化此过程。看看:http://api.qunitjs.com/asyncTest/

在Jasmine(http://pivotal.github.com/jasmine/),一个行为驱动开发(BDD)测试框架中,有内置的方法来编写异步测试。这是Jasmine中异步测试的一个例子:

describe('Some module', function() {

    it('should run asynchronously', function() {
        var isDone = false;
        runs(function() {
            // The first call to runs should trigger some async operation
            // that has a side-effect that can be tested for. In this case,
            // lets say that the doSomethingAsyncWithCallback function
            // does something asynchronously and then calls the passed callback
            doSomethingAsyncWithCallback(function() { isDone = true; });
        });

        waitsFor(function() {
            // The call to waits for is a polling function that will get called
            // periodically until either a condition is met (the function should return
            // a boolean testing for this condition) or the timeout expires.
            // The optional text is what error to display if the test fails.
            return isDone === true;
        }, "Should set isDone to true", 500);

        runs(function() {
            // The second call to runs should contain any assertions you need to make
            // after the async call is complete.
            expect(isDone).toBe(true);
        });
    });
});

编辑:此外,Jasmine有几种内置方法可以伪造浏览器的setTimeout和setInterval函数,而无需在套件中进行任何其他可能依赖于此的测试。我会看一下使用它们而不是手动覆盖setTimeout / setInterval函数。

答案 5 :(得分:0)

一旦你的函数返回(在为下次运行设置超时之后),javascript将查看下一个需要运行并运行它的东西。

据我所知,javascript中的'主线程'只是一个响应事件的循环(例如脚本标记的onload,它运行该标记的内容)。

基于以上两个条件,尽管有任何setTimeout,调用线程总是会运行完成,并且这些超时将在调用线程没有任何剩余运行之后开始。

我测试的方法是在调用()

之后立即运行以下函数
function looper(name,duration) {
    var start = (new Date()).getTime();
    var elapsed = 0;
    while (elapsed < duration) {
        elapsed = (new Date()).getTime() - start;
        console.log(name + ": " + elapsed);
    }        
}

持续时间应设置为某个时间段(a)中的setTimeout持续时间。预期的输出将是'looper'的输出,然后是重复调用a()的输出。

接下来要测试的是,当()及其子调用正在执行时,其他脚本标记是否能够运行。

你可以这样做:

<script>
    a(); 
</script>
<script>
    looper('delay',500); // ie; less than the 1000 timeout in a();
</script>
<script>
    console.log('OK');
</script>

尽管a()及其子节点仍在执行,但您希望“OK”出现在日志中。您还可以测试此变体,例如window.onload()等。

最后,您还需要确保其他计时器事件也能正常运行。简单地将2个呼叫延迟半秒并检查它们是否交错应该表明工作正常:

function b()
{
    console.log("invoked b")
    setTimeout(b, 1000);
}

a();
looper('wait',500);
b();

应该产生类似

的输出
invoked
invoked b
invoked
invoked b
invoked
invoked b

希望这就是你要找的东西!

如果您需要有关如何在Qunit中执行此操作的技术详细信息,请编辑:

如果Qunit无法捕获console.log输出(我不确定),只需将这些字符串推入数组或字符串中,然后在运行后检查它。您可以在测试模块()设置中覆盖console.log并在拆卸时恢复它。我不确定Qunit是如何工作的,但是'this'可能必须被删除,并且全局变量用于存储old_console_log和test_output

// in the setup
this.old_console_log = console.log;
this.test_output = [];
var self = this;
console.log = function(text) { self.test_output.push(text); }

// in the teardown
console.log = this.old_console_log;

最后,您可以使用stop()和start(),以便Qunit知道等待测试中的所有事件完成运行。

stop();
kickoff_async_test();
setTimeout(function(){
    // assertions
    start();
    },<expected duration of run>);

答案 6 :(得分:0)

基于所有答案,我想出了适合我案例的解决方案:

testAsync("Doesn't hang", function(){
    expect(1);

    var ranToLong = false;
    var last = new Date();
    var sched = setInterval(function(){
        var now = new Date();
        ranToLong = ranToLong || (now - last) >= 50;
        last = now;
    }, 0);

    // In this case, asyncRecursiveFunction runs for a long time and 
    // returns a single value in callback
    asyncRecursiveFunction(function callback(v){
        clearInterval(sched);
        var now = new Date();
        ranToLong = ranToLong || (now - last) >= 50;
        assert.equal(ranToLong, false);
        start();
    });
});

它通过查看另一个预定函数调用之间的时间来测试'asyncRecursiveFunction'在处理时不会挂起。

这真的很难看,并不适用于所有情况,但它似乎对我有用,因为我可以将我的函数限制为一些大的异步递归调用,因此它运行很长但不是无限时间。正如我在问题中所提到的,我很高兴证明这些案件不会阻止。

顺便说一句,有问题的实际代码可在gen.js中找到。主要问题是异步减少生成器。它正确地异步返回一个值,但是由于同步内部实现,以前的版本会停止。