2

Javascriptには、2つのバージョンの再帰関数があります。1つは同期的に実行され、もう1つは単純なスケジューリングを使用して非同期的に実行されます。特定の入力が与えられると、どちらの場合も、関数は無限の実行パスを持つことが期待されます。これらの関数のテスト、具体的には非同期バージョンがメインスレッドをブロックしないことを確認するためのテストを開発する必要があります。

返されない場合にこれらの関数の出力コールバック動作をチェックするテストがすでにあります。ブロック動作のテストのみに関心があります。テストの目的でも、関数の実行時間を長くすることもできますが、有限の時間に制限することもできます。現在QUnitを使用していますが、別のテストフレームワークに切り替えることができます。

戻ってこない非同期関数がブロックしないことをどのようにテストできますか?

編集、明確化のため

これは、私が使用している関数の骨の折れる例です。

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

a();

問題を最も明確に表現していると感じたため、説明の一部のスレッド用語を意図的に誤用しています。メインスレッドをブロックしないということは、関数を呼び出しても他のロジックのスケジューリングと実行が妨げられないことを意味します。関数自体はメインスレッドで実行されると思いますが、将来実行される予定である限り、関数は実行されていると思います。

4

7 に答える 7

2

単体テストは、単一責任の原則と分離に基づいています(テスト対象の対象をその依存関係から分離します)。

この場合、関数が非同期で実行されることを期待しますが、この動作は関数によって実行されず、「setTimeout」関数によって実行されます。したがって、依存関係ではないため、関数を「setTimeout」から分離する必要があると思います。テストしたい場合、ブラウザはそれが機能することを保証します。

次に、「setTimeout」が非同期ロジックを実行すると信頼しているため、「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を使用することをお勧めします。これは、関数であるスパイのような多くのツールを提供し、何回、どの引数が呼び出されたかを通知します。

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は、setTimeout置換を行う偽のタイマーも提供しますが、「x」ミリ秒をシミュレートする.tick(x)メソッドなど、はるかに多くの機能を備えていますが、この場合は役に立ちません。

質問の編集に回答するための更新:

1-関数は無限に実行されるため、実行を中断せずにテストすることはできません。そのため、「setTimeout」をどこかで上書きする必要があります。

2-関数を再帰的に実行して、反復の間に他のコードを実行できるようにしますか?すごい!ただし、関数がこれを実行できないことを理解してください。関数はsetTimeoutまたはsetIntervalを呼び出すことしかできず、この関数が期待どおりに機能することを期待しています。関数が何をするかをテストする必要があります。

3-別のJavascriptコードが使用するJavascript(サンドボックス環境)からテストし、1つの実行スレッド(テストに使用しているものと同じ)のみを解放したい。これは本当に簡単なテストだと思いますか?

4-しかし最も重要なもの-ホワイトボックスはテストと依存関係を結合するため、ホワイトボックスは好きではありません。依存関係を変更したり、将来どのように呼び出されるかを変更する場合は、テストを変更する必要があります。この問題はDOM関数には存在しません。DOM関数は何年も同じインターフェイスを維持します。今のところ、これら2つの関数のいずれかを呼び出す以外に、やりたいことを行う方法がないので、これについては考えていません。ケース「ホワイトボックステスト」は悪い考えです。

これは、Promiseパターンの実装のテストで、Promiseがすでに実行されている場合でも、常に非同期である必要があるのと同じ問題があり、テストエンジンの非同期テスト方法(コールバックなどを使用)を使用してテストしたためです。そしてそれは混乱で、テストはランダムに失敗し、テストの実行が非常に遅くなりました。次に、TDDの専門家に、どのようにテストを難しくすることができるかを尋ねたところ、promiseの実装とsetTimeoutの動作をテストしようとしていたため、単一責任の原則に従わなかったよりも彼は答えました。

于 2012-10-06T00:00:40.237 に答える
1

無限に実行される実行パスがブロックされないことをテストまたは証明することはほぼ不可能であるため、問題を部分に分割する必要があります。

あなたの道は基本的foo(foo(foo(foo(...etc...))))に、SetTimeout実際に再帰を取り除くことを気にしないでください。したがって、あなたがしなければならないのは、あなたのfooがブロックされないことをテストまたは証明することだけです(テストは証明するよりも「少し」簡単になるでしょう、以下で詳しく説明します)

それで、関数はfooブロックしますか?

少し数学的に言えば、常に値があるかどうかを知りたい場合は、実際には、返すことができるすべての値が常にf(f(...f(x)...))あることを証明するだけで済みます。戻り値が適切であることを確認できれば、再帰の数は関係ありません。f(x)xf

それがあなたにとって意味することは、それが可能な入力値をブロックしないfooことを証明するだけでよいということです。fooこの場合、すべてのグローバル変数とクロージャも入力値であることに注意してください。これは、すべての呼び出しで使用しているすべての値を健全性チェックする必要があることを意味します。

テストするには、もちろんSetTimeoutを置き換える必要がありますが、それは簡単です。空の関数(function(){})に置き換えると、この関数が実行をブロックしたり変更したりしないことを簡単に証明できます。その後、

物事を簡単にする

私が上で書いたことを取り入れると、これはまた、これまで使用していたグローバル関数または変数が、関数が壊れてしまうポイントに変更されないようにする必要があることを意味します。これは実際には非常に難しいことですが、常に同じ関数と値を使用し、クロージャを使用して他の関数がそれらに触れないようにすることで、作業を簡単にすることができます。

function foo(n, setTimeout)
{
   var x = global_var;
   // sanity check n here
   function f()
   {
      setTimeout(f, n)
   }
   return f();
}
  • このように、最初の実行でこれらの値をテストするだけで済みます。Math.Piを含む文字列値ではなく、実際にはPiであると想定できるのは素晴らしいことです"noodles"。すごくいい。
  • グローバルな可変オブジェクトを使用しないでください
  • 使用を回避できない人に電話してsetTimeout、ブロックできないようにします

    • 戻り値が必要な場合、物事は本当にトリッキーになりますが、可能です。これを検討してください。

      function() {
        var x = 0;
        setTimeout(function(){x = insecure();}, 1); 
      }
      

      あなたがしなければならないのは

      • x次の反復を使用
      • 最初にxの健全性チェック値!
  • SetTimeoutはブロックされますか?

    もちろん、これはsetTimeoutがブロックするかどうかによって異なります。これを証明するのは非常に難しいですが、テストするのは少し簡単です。実装はインタプリタ次第なので、実際に証明することはできません。

    個人的には、戻り値が破棄されると、setTimeoutは空の関数のように動作すると思います。

于 2012-10-12T17:17:15.563 に答える
1

ビヘイビア駆動テストの観点から考えると、「関数はブロックされますか?」有用な質問ではありません。それは間違いなくブロックされます、より良い質問は「50ms以内に戻るか」かもしれません。

あなたは次のようなものでこれを行うことができます:

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

これに関する問題は、誰かがあなたの関数ブロックを無期限に失敗させないような愚かなことをした場合、それはハングするということです。

JavaScriptはシングルスレッドであるため、これを回避する方法はありません。私がやって来て、あなたの関数を次のように変更した場合:

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

テストがハングします。

物事を少しリファクタリングすることで、この方法で物事を壊すのを難しくすることができます。行われている2つの別々のことがあります。あなたの仕事の塊とスケジューリング。これらを分離すると、次のような関数になります。

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

これで、すべてを個別にテストできます。模擬workFuncとscheduleFuncをdoAsyncのテストに提供して、期待どおりに動作することを確認できます。また、関数a()を、スケジュール方法を気にせずにテストできます。

劣等生プログラマーが関数a()に無限ループを入れることはまだ可能ですが、それ以上の作業単位を実行する方法を考慮する必要がないため、その可能性は低くなります。

于 2012-10-12T02:32:30.847 に答える
0

基本的に、JavaScriptはシングルスレッドであるため、メインスレッドブロックします。だが :

  • 関数のスケジュールに使用していると想定しているsetTimesoutので、その関数の呼び出しにそれほど時間がかからない場合(たとえば、200または300ミリ秒未満)、ユーザーには気付かれません。

  • その関数(CanvasまたはWebGLを含む)の間にDOM操作を行っている場合は、失敗します。ただし、そうでない場合は、UIをブロックしないことが保証されている個別のスレッドを生成できるWebワーカーを調べることができます。

しかし、とにかく、JavaScriptとメインループは、ここ数か月間私を悩ませてきたトリッキーな問題なので、あなただけではありません!

于 2012-10-05T20:16:23.870 に答える
0

この非同期テストの実行はQUnitで実際に可能ですが、別のJavaScriptテストフレームワークであるJasmineJSでより適切に処理されます。両方の例を示します。

QUnitでは、最初にstop()関数を呼び出して、テストが非同期で実行されることが期待されることを通知する必要があります。次に、期待値を含む関数を使用してsetTimeoutを呼び出す必要があります。また、start()関数を呼び出してブロックを完了する必要があります。 。次に例を示します。

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

編集:どうやら、このプロセスを簡素化するために使用できるasyncTestコンストラクト全体もあるようです。ご覧ください:http ://api.qunitjs.com/asyncTest/

ビヘイビア駆動開発(BDD)テストフレームワークであるJasmine(http://pivotal.github.com/jasmine/)には、非同期テストを作成するための組み込みメソッドがあります。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関数を手動でオーバーライドするのではなく、これらを使用する方法を検討します。

于 2012-10-07T20:20:41.723 に答える
0

関数が戻るとすぐに(次の実行のタイムアウトを設定した後)、javascriptは実行が必要な次のことを調べて実行します。

私が知る限り、javascriptの「メインスレッド」は、イベント(そのタグのコンテンツを実行するスクリプトタグのonloadなど)に応答する単なるループです。

上記の2つの条件に基づいて、呼び出しスレッドはsetTimeoutsに関係なく常に完了まで実行され、これらのタイムアウトは呼び出しスレッドに実行するものがなくなった後に開始されます。

これをテストした方法は、a()の呼び出しの直後に次の関数を実行することでした。

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()の繰り返し呼び出しの出力になります。

次にテストするのは、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つの呼び出しを0.5秒遅らせ、それらがインターリーブすることを確認するだけで、正常に機能することが示されます。

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出力をキャプチャできない場合(よくわかりません)、それらの文字列を配列または文字列にプッシュして、実行後に確認してください。テストmodule()セットアップで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>);
于 2012-10-12T05:07:38.797 に答える
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にあります。主な問題は非同期リデュースジェネレーターでした。非同期で正しく値を返しましたが、以前のバージョンでは、同期内部実装のために停止していました。

于 2012-10-16T04:19:17.537 に答える