テスト可能なJavaScriptを書く

[注 trans。]:元Twitter開発者のBen Cherryによる記事の翻訳に注目します。 この記事では、テストに適したJavaScriptコードを作成するためのヒントをいくつか示しています。



Twitter開発文化では、テストを書く必要があります。 Twitterで作業する前にJavascriptをテストした経験はなかったので、多くを学ぶ必要がありました。 特に、私が使用していたプログラミングパターンの一部は、その使用について記述し、推奨していましたが、テストには不適切でした。 したがって、テスト可能なJavascriptコードを作成するために開発した最も重要な原則のいくつかを共有する価値があると思いました。 私が提供する例はQUnitに基づいていますが、Javascriptをテストするための任意のフレームワークに適用できます。



シングルトーンを避ける



私の最も人気のある投稿の1つは、javascript テンプレートモジュールを使用してアプリケーションでシングルトーンを作成する方法に関するものでした。 このアプローチは簡単で便利ですが、1つの単純な理由でテストの問題が発生します:シングルトンはテスト間でオブジェクトの状態を汚染します。 モジュール形式のシングルトンの代わりに、構築されたオブジェクトとして作成し、アプリケーションの初期化中にグローバルレベルのインスタンスに割り当てる必要があります。



たとえば、次のシングルトンモジュールを考えてみましょう(もちろん、この例は架空のものです)。



var dataStore = (function() { var data = []; return { push: function (item) { data.push(item); }, pop: function() { return data.pop(); }, length: function() { return data.length; } }; }());
      
      





このモジュールでは、いくつかのメソッドをテストできます。 簡単なQUnitテストの例を次に示します。



 module("dataStore"); test("pop", function() { dataStore.push("foo"); dataStore.push("bar") equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item"); }); test("length", function() { dataStore.push("foo"); equal(dataStore.length(), 1, "adding 1 item makes the length 1"); });
      
      





このテストスイートの実行中、「長さ」メソッドの検証は失敗しますが、それを見ると、その理由は明らかになりません。 問題は、前回のテスト後にdataStoreオブジェクトの状態が保持されたことです。 テストの順序を変更するだけで、両方のテストがテストに合格します。これは、何かが間違っていることの明らかな兆候です。 各テストの前にdataStoreオブジェクトの状態を返すことでこれを修正できますが、これは、モジュールに変更を加えた場合、テスト用のテンプレートを常に維持する必要があることを意味します。 別のアプローチを使用することをお勧めします。



 function newDataStore() { var data = []; return { push: function (item) { data.push(item); }, pop: function() { return data.pop(); }, length: function() { return data.length; } }; } var dataStore = newDataStore();
      
      





これで、テストスイートは次のようになります。



 module("dataStore"); test("pop", function() { var dataStore = newDataStore(); dataStore.push("foo"); dataStore.push("bar") equal(dataStore.pop(), "bar", "popping returns the most-recently pushed item"); }); test("length", function() { var dataStore = newDataStore(); dataStore.push("foo"); equal(dataStore.length(), 1, "adding 1 item makes the length 1"); });
      
      





このアプローチにより、グローバルオブジェクトは以前と同じように動作できますが、テストは互いに詰まりません。 各テストには独自のdataStoreオブジェクトのインスタンスがあり、テストが完了するとガベージコレクターによって破棄されます。



クロージャーでプライベートプロパティを作成しない


私が推進するもう1つのパターンは、Javascriptでの真のプライベートプロパティの作成です。 この方法の利点は、グローバルな名前空間を、隠された実装の詳細への不必要な参照から解放できることです。 ただし、このプログラミングパターンを悪用すると、コードがテストに適さなくなる可能性があります。 この現象の理由は、テストスイートがクロージャーに隠されたプライベート関数にアクセスできないため、それらをテストできないことです。 例を考えてみましょう:



 function Templater() { function supplant(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; } var templates = {}; this.defineTemplate = function(name, template) { templates[name] = template; }; this.render = function(name, params) { if (typeof templates[name] !== "string") { throw "Template " + name + " not found!"; } return supplant(templates[name], params); }; }
      
      





Templaterオブジェクトのキーメソッドは「代替」ですが、関数のクロージャ以外ではアクセスできません。 したがって、計画どおりに機能するかどうかを確認することはできません。 さらに、「render」メソッドを呼び出さずに「defineTemplate」メソッドが何もしないかどうかを確認できません。 「getTemplate()」メソッドを追加できますが、テスト目的でのみパブリックインターフェイスにメソッドを追加したことがわかりますが、これは良いアプローチではありません。 このような状況では、重要なプライベートメソッドを持つ複雑なオブジェクトを構築すると、テストできないコードに依存しなければならないという事実につながります。これは非常に危険です。 以下は、このオブジェクトのテスト可能なバージョンの例です。



 function Templater() { this._templates = {}; } Templater.prototype = { _supplant: function(str, params) { for (var prop in params) { str.split("{" + prop +"}").join(params[prop]); } return str; }, render: function(name, params) { if (typeof this._templates[name] !== "string") { throw "Template " + name + " not found!"; } return this._supplant(this._templates[name], params); }, defineTemplate: function(name, template) { this._templates[name] = template; } };
      
      





そして、これはQUnitテストのセットです:



 module("Templater"); test("_supplant", function() { var templater = new Templater(); equal(templater._supplant("{foo}", {foo: "bar"}), "bar")) equal(templater._supplant("foo {bar}", {bar: "baz"}), "foo baz")); }); test("defineTemplate", function() { var templater = new Templater(); templater.defineTemplate("foo", "{foo}"); equal(template._templates.foo, "{foo}"); }); test("render", function() { var templater = new Templater(); templater.defineTemplate("hello", "hello {world}!"); equal(templater.render("hello", {world: "internet"}), "hello internet!"); });
      
      





renderメソッドのテストは、defineTemplateメソッドとsupplantメソッドが互いに正しく補完することを確認することのみを目的としていることに注意してください。 すでにそれらを互いに別々にテストしているため、テストが失敗した場合にどのコンポーネントが正しく機能していないかを簡単に理解できます。



関連する関数を書く


関連する関数はどの言語でも重要ですが、Javascriptにはこのための独自の理由があります。 Javascriptで行うことの多くは、テストスイートが依存する環境によって提供されるグローバルオブジェクトを使用します。 たとえば、すべてのメソッドはwindow.locationに関連付けられるため、URLを変更する関数のテストは困難です。 代わりに、次に何をすべきかを決定するのに役立つ論理コンポーネントにシステムを分割し、それを実行する短い関数を作成する必要があります。 さまざまな着信データと発信データで論理関数をテストし、window.locationを変更する関数をテストせずに残すことができます。 システムが正しく構成されていれば、このアプローチは安全です。



不適切なコードの例を次に示します。



 function redirectTo(url) { if (url.charAt(0) === "#") { window.location.hash = url; } else if (url.charAt(0) === "/") { window.location.pathname = url; } else { window.location.href = url; } }
      
      





この例のロジックは非常に単純ですが、より複雑な状況を想像できます。 複雑さが増すにつれて、ブラウザウィンドウをリダイレクトせずにこのメソッドをテストすることはできなくなります。



そしてここに良いバージョンがあります:



 function _getRedirectPart(url) { if (url.charAt(0) === "#") { return "hash"; } else if (url.charAt(0) === "/") { return "pathname"; } else { return "href"; } } function redirectTo(url) { window.location[_getRedirectPart(url)] = url; }
      
      





これで、「_ getRedirectPart」の簡単なテストスイートを作成できます。



 test("_getRedirectPart", function() { equal(_getRedirectPart("#foo"), "hash"); equal(_getRedirectPart("/foo"), "pathname"); equal(_getRedirectPart("http://foo.com"), "href"); });
      
      





この場合、「redirectTo」メソッドの主要部分がテストされ、ランダムリダイレクトを心配することはできません。



たくさんのテストを書く


これは簡単な作業ではありませんが、これを常に覚えておくことは非常に重要です。 多くのプログラマーが書くテストは少なすぎます。それらを書くのは時間がかかり、時間がかかるからです。 私は常にこの問題に苦しんでいるので、テストを書くプロセスを少し簡単にするQUnitの小さなヘルパーを作成しました。 これは関数「testCases()」であり、関数、実行コンテキスト、および比較用の入力/出力データの配列を渡すことにより、テストブロック内で呼び出すことができます。 これにより、テストスイートを簡単に作成できます。



 function testCases(fn, context, tests) { for (var i = 0; i < tests.length; i++) { same(fn.apply(context, tests[i][0]), tests[i][1], tests[i][2] || JSON.stringify(tests[i])); } }
      
      





使用例:



 test("foo", function() { testCases(foo, null, [ [["bar", "baz"], "barbaz"], [["bar", "bar"], "barbar", "a passing test"] ]); });
      
      







結論


Javascriptのテストについてはまだ多くのことを書くことができ、このテーマに関する多くの優れた本があると確信していますが、日常の仕事で出会う実際的な例の概要を説明したいと思います。 私はテストの専門家ではないので、間違いを犯したか、悪いアドバイスをしたかどうかを教えてください。



All Articles