JavaScriptでテスト可能なコードを書く:簡単な概要
公開: 2022-03-11ノードをMochaやJasmineなどのテストフレームワークと組み合わせて使用する場合でも、PhantomJSなどのヘッドレスブラウザーでDOM依存のテストをスピンアップする場合でも、JavaScriptの単体テストのオプションはこれまでになく優れています。
ただし、これは、テストしているコードがツールと同じくらい簡単であることを意味するものではありません。 簡単にテストできるコードの整理と記述には、ある程度の労力と計画が必要ですが、関数型プログラミングの概念に触発されたパターンがいくつかあり、コードをテストするときに困難な状況に陥らないようにするために使用できます。 この記事では、JavaScriptでテスト可能なコードを作成するためのいくつかの役立つヒントとパターンについて説明します。
ビジネスロジックとディスプレイロジックを分離する
JavaScriptベースのブラウザアプリケーションの主な仕事の1つは、エンドユーザーによってトリガーされたDOMイベントをリッスンし、ビジネスロジックを実行して結果をページに表示することで、それらに応答することです。 DOMイベントリスナーを設定している場所で作業の大部分を正しく実行する無名関数を作成するのは魅力的です。 これが引き起こす問題は、匿名関数をテストするためにDOMイベントをシミュレートする必要があることです。 これにより、コード行とテストの実行にかかる時間の両方でオーバーヘッドが発生する可能性があります。
代わりに、名前付き関数を記述して、それをイベントハンドラーに渡します。 そうすれば、フープを飛び越えて偽のDOMイベントをトリガーすることなく、名前付き関数のテストを直接作成できます。
ただし、これはDOM以外にも当てはまります。 ブラウザとノードの両方にある多くのAPIは、イベントを起動してリッスンするか、他の種類の非同期作業が完了するのを待つことを中心に設計されています。 経験則として、匿名のコールバック関数を多数作成している場合、コードのテストは簡単ではない可能性があります。
// hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }
非同期コードでコールバックまたはプロミスを使用する
上記のコード例では、リファクタリングされたfetchThings関数がAJAXリクエストを実行し、ほとんどの作業を非同期で実行します。 これは、関数の実行がいつ終了したかわからないため、関数を実行して、期待したすべてのことを実行したことをテストできないことを意味します。
この問題を解決する最も一般的な方法は、非同期で実行される関数にパラメーターとしてコールバック関数を渡すことです。 単体テストでは、渡したコールバックでアサーションを実行できます。
非同期コードを整理するためのもう1つの一般的で人気が高まっている方法は、PromiseAPIを使用することです。 幸い、$。ajaxと他のほとんどのjQueryの非同期関数はすでにPromiseオブジェクトを返すため、多くの一般的なユースケースがすでにカバーされています。
// hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }
副作用を避ける
結果を得るために数式に数値を打ち込むのと同じように、引数を取り、それらの引数のみに基づいて値を返す関数を記述します。 関数が外部状態(クラスインスタンスのプロパティやファイルの内容など)に依存していて、関数をテストする前にその状態を設定する必要がある場合は、テストでさらに設定を行う必要があります。 実行されている他のコードが同じ状態を変更していないことを信頼する必要があります。
同様に、実行中に外部状態を変更する関数(ファイルへの書き込みやデータベースへの値の保存など)の書き込みは避けてください。 これにより、他のコードを自信を持ってテストする能力に影響を与える可能性のある副作用を防ぐことができます。 一般に、副作用をコードの端にできるだけ近づけ、「表面積」をできるだけ少なくするのが最善です。 クラスとオブジェクトインスタンスの場合、クラスメソッドの副作用は、テストされるクラスインスタンスの状態に限定する必要があります。
// hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }
依存性注入を使用する
関数の外部状態の使用を減らすための一般的なパターンの1つは、依存性注入です。つまり、関数のすべての外部ニーズを関数パラメーターとして渡します。

// depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }
依存性注入を使用する主な利点の1つは、実際の副作用(この場合はデータベース行の更新)を引き起こさない単体テストからモックオブジェクトを渡すことができ、モックオブジェクトが実行されたことを表明できることです。期待通りに。
各機能に単一の目的を与える
いくつかのことを行う長い関数を、短い単一目的の関数のコレクションに分割します。 これにより、値を返す前に大きな関数がすべてを正しく実行していることを期待するのではなく、各関数がその役割を正しく実行していることをテストするのがはるかに簡単になります。
関数型プログラミングでは、いくつかの単一目的関数をつなぎ合わせる行為は、合成と呼ばれます。 Underscore.jsには関数_.compose
もあります。この関数は、関数のリストを取得してそれらをチェーンし、各ステップの戻り値を取得して、次の関数に渡します。
// hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }
パラメータを変更しないでください
JavaScriptでは、配列とオブジェクトは値ではなく参照によって渡され、変更可能です。 つまり、オブジェクトまたは配列をパラメーターとして関数に渡すと、コードと、オブジェクトまたは配列を渡した関数の両方で、メモリ内のその配列またはオブジェクトの同じインスタンスを変更できるようになります。 つまり、独自のコードをテストしている場合は、コードが呼び出す関数がオブジェクトを変更していないことを信頼する必要があります。 同じオブジェクトを変更する新しい場所をコードに追加するたびに、そのオブジェクトがどのように見えるかを追跡することがますます難しくなり、テストが難しくなります。
代わりに、オブジェクトまたは配列を受け取る関数がある場合は、読み取り専用であるかのように、そのオブジェクトまたは配列に作用させます。 コードで新しいオブジェクトまたは配列を作成し、必要に応じてそれに値を追加します。 または、UnderscoreまたはLodashを使用して、渡されたオブジェクトまたは配列を操作する前に複製します。 さらに良いことに、読み取り専用のデータ構造を作成するImmutable.jsのようなツールを使用します。
// alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }
コードの前にテストを書く
テストするコードの前に単体テストを作成するプロセスは、テスト駆動開発(TDD)と呼ばれます。 多くの開発者は、TDDが非常に役立つと感じています。
最初にテストを作成することにより、公開しているAPIについて、それを使用する開発者の観点から考える必要があります。 また、不必要に複雑なソリューションを過剰に設計するのではなく、テストによって実施される契約を満たすのに十分なコードのみを記述していることを確認するのにも役立ちます。
実際には、TDDは、すべてのコード変更に対してコミットするのが難しい分野です。 しかし、試す価値があると思われる場合は、すべてのコードをテスト可能に保つことを保証するための優れた方法です。
要約
複雑なJavaScriptアプリを作成してテストするときに、非常に簡単に陥る落とし穴がいくつかあることは誰もが知っています。 しかし、うまくいけば、これらのヒントを使用し、コードを可能な限りシンプルで機能的に保つことを忘れないでください。テストカバレッジを高く保ち、コード全体の複雑度を低く抑えることができます。
- JavaScript開発者が犯す最も一般的な10の間違い
- スピードの必要性:トップタルJavaScriptコーディングチャレンジの回顧展