自動テストは今後のタスクでも使用される予定で、実際のプロジェクトでも広く使用されています。
関数を書くとき、私たちは通常、関数が何をすべきか、つまりどのパラメータがどの結果を与えるかを想像することができます。
開発中に、関数を実行し、その結果を期待される結果と比較することで、関数をチェックできます。たとえば、コンソールで実行できます。
何か問題がある場合は、コードを修正し、再度実行して結果を確認し、動作するまでこれを繰り返します。
しかし、このような手動の「再実行」は不完全です。
手動でコードを再実行してテストする場合、何かを見落としがちです。
たとえば、関数f
作成しています。コードを書いてテストしました。f f(1)
機能しますが、 f(2)
機能しません。コードを修正すると、 f(2)
動作するようになりました。完成したように見えますか?しかし、 f(1)
再テストするのを忘れていました。エラーが発生する可能性があります。
それは非常に典型的です。何かを開発するとき、私たちは考えられる多くのユースケースを念頭に置きます。しかし、変更のたびにプログラマーがすべてを手動でチェックすることを期待するのは困難です。そのため、あるものを修正しても別のものを壊すことは簡単になります。
自動テストとは、コードとは別にテストが個別に記述されることを意味します。彼らはさまざまな方法で関数を実行し、結果と期待値を比較します。
まず、行動駆動開発、つまり BDD と呼ばれる手法から始めましょう。
BDD は、テスト、ドキュメント、サンプルの 3 つを 1 つにまとめたものです。
BDD を理解するために、実際の開発事例を見てみましょう。
x
整数n
乗する関数pow(x, n)
を作成したいとします。 n≥0
と仮定します。
このタスクはほんの一例です。JavaScript にはこれを実行できる**
演算子がありますが、ここではより複雑なタスクにも適用できる開発フローに焦点を当てます。
pow
のコードを作成する前に、関数が何を行うべきかを想像し、それを記述することができます。
このような記述は仕様、つまり仕様と呼ばれ、次のようにユースケースの説明とそのテストが含まれます。
description("pow", function() { it("n 乗する", function() { アサート.equal(pow(2, 3), 8); }); });
仕様には、上に示した 3 つの主要な構成要素があります。
describe("title", function() { ... })
どのような機能について説明しているのでしょうか?この例では、関数pow
説明しています。 「ワーカー」( it
)をグループ化するために使用されます。
it("use case description", function() { ... })
it
では特定の使用例を人間が判読できる方法で説明しており、2 番目の引数はそれをテストする関数です。
assert.equal(value1, value2)
it
内のコードは、実装が正しい場合、エラーなしで実行されるはずです。
関数assert.*
は、 pow
期待どおりに動作するかどうかをチェックするために使用されます。ここではそのうちの 1 つであるassert.equal
使用しています。これは引数を比較し、等しくない場合はエラーを生成します。ここでは、 pow(2, 3)
の結果が8
に等しいかどうかをチェックします。他のタイプの比較とチェックもありますが、これらは後で追加します。
この仕様は実行可能であり、 it
ブロックで指定されたテストが実行されます。それについては後で説明します。
通常、開発の流れは次のようになります。
最も基本的な機能のテストを含む初期仕様が作成されます。
初期実装が作成されます。
それが機能するかどうかを確認するために、仕様を実行するテスト フレームワーク Mocha (詳細は近日中に) を実行します。機能が完了していない間は、エラーが表示されます。すべてが機能するまで修正を加えます。
これで、テストを伴う動作する初期実装が完成しました。
おそらく実装ではまだサポートされていないユースケースを仕様に追加します。テストが失敗し始めます。
3 に進み、テストでエラーがなくなるまで実装を更新します。
機能の準備が整うまで、手順 3 ~ 6 を繰り返します。
したがって、開発は反復的です。仕様を作成し、実装して、テストが合格することを確認し、さらにテストを作成して、それらが動作することを確認します。最終的には、動作する実装とそのテストの両方が完成します。
この開発フローを実際のケースで見てみましょう。
最初のステップはすでに完了しています。 pow
の初期仕様が完成しました。さて、実装を行う前に、いくつかの JavaScript ライブラリを使用してテストを実行して、それらが機能することを確認してみましょう (すべて失敗します)。
このチュートリアルでは、テストに次の JavaScript ライブラリを使用します。
Mocha – コア フレームワーク: これは、 describe
やit
、テストを実行する main 関数などの一般的なテスト関数を提供します。
Chai – 主張の多いライブラリ。これにより、さまざまなアサーションを使用できるようになります。現時点では、 assert.equal
のみが必要です。
Sinon – 関数を監視したり、組み込み関数をエミュレートしたりするためのライブラリです。これはずっと後になって必要になります。
これらのライブラリは、ブラウザ内テストとサーバー側テストの両方に適しています。ここではブラウザのバリアントについて考えてみましょう。
これらのフレームワークとpow
仕様を含む完全な HTML ページ:
<!DOCTYPE html> <html> <頭> <!-- 結果を表示するには、mocha CSS を追加します --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- mocha フレームワーク コードを追加 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <スクリプト> mocha.setup('bdd'); // 最小限のセットアップ </script> <!-- チャイを追加 --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <スクリプト> // チャイにはたくさんのものがあるので、assert をグローバルにしましょう letassert = chai.assert; </script> </head> <本文> <スクリプト> 関数 pow(x, n) { /* 関数コードを書き込むため、今は空です */ } </script> <!-- テストを含むスクリプト (説明してください...) --> <script src="test.js"></script> <!-- id="mocha" の要素にはテスト結果が含まれます --> <div id="モカ"></div> <!-- テストを実行してください! --> <スクリプト> mocha.run(); </script> </body> </html>
このページは 5 つの部分に分けることができます。
<head>
– テスト用のサードパーティのライブラリとスタイルを追加します。
<script>
にはテストする関数が含まれます。この場合、 pow
のコードが含まれます。
テスト – この場合は、上記のdescribe("pow", ...)
を持つ外部スクリプトtest.js
です。
HTML 要素<div id="mocha">
は、Mocha が結果を出力するために使用します。
テストはコマンドmocha.run()
によって開始されます。
結果:
現時点では、テストは失敗し、エラーが発生しています。これは論理的です。 pow
には空の関数コードがあるため、 pow(2,3)
8
の代わりにundefined
を返します。
将来的には、karma やその他のさまざまなテストを簡単に自動実行できる高レベルのテスト ランナーがさらに存在することに注意してください。
テストに合格するために、 pow
の簡単な実装を作成しましょう。
関数 pow(x, n) { 8を返します。 // :) 私たちは騙します! }
うわー、うまくいくようになりました!
私たちがやったことは間違いなく詐欺です。関数は機能しません。pow pow(3,4)
を計算しようとすると間違った結果が得られますが、テストは合格します。
…しかし、この状況は非常に典型的なものであり、実際に起こります。テストは合格しますが、関数が正しく動作しません。私たちの仕様は不完全です。さらに多くのユースケースを追加する必要があります。
pow(3, 4) = 81
あることを確認するテストをもう 1 つ追加してみましょう。
ここでテストを編成する 2 つの方法のいずれかを選択できます。
最初のバリアント – 同じit
にもう 1 つのassert
を追加します。
description("pow", function() { it("n 乗", function() { アサート.equal(pow(2, 3), 8); アサート.equal(pow(3, 4), 81); }); });
2 番目 – 2 つのテストを実行します。
description("pow", function() { it("2の3乗は8", function() { アサート.equal(pow(2, 3), 8); }); it("3 の 4 乗は 81", function() { アサート.equal(pow(3, 4), 81); }); });
主な違いは、 assert
エラーをトリガーすると、 it
ブロックが即座に終了することです。したがって、最初のバリアントでは、最初のassert
が失敗した場合、2 番目のassert
の結果は表示されません。
テストを分離すると、何が起こっているかについてより多くの情報を得るのに役立つため、2 番目のバリアントの方が優れています。
それに加えて、従うべきルールがもう 1 つあります。
1 つのテストで 1 つのことをチェックします。
テストを見て、2 つの独立したチェックが含まれている場合は、より単純な 2 つのチェックに分割する方がよいでしょう。
それでは、2 番目のバリエーションを続けましょう。
結果:
予想通り、2 番目のテストは失敗しました。確かに、この関数は常に8
返しますが、 assert
81
期待しています。
テストに合格するために、より現実的なものを書いてみましょう。
関数 pow(x, n) { 結果 = 1 とします。 for (let i = 0; i < n; i++) { 結果 *= x; } 結果を返します。 }
関数が適切に動作することを確認するために、さらに多くの値をテストしてみましょう。ブロックを手動でit
代わりに、 for
でブロックを生成できます。
description("pow", function() { 関数 makeTest(x) { 期待値 = x * x * x; とします。 it(`${x} の 3 乗は ${expected}`, function() { assert.equal(pow(x, 3), Expected); }); } for (let x = 1; x <= 5; x++) { makeTest(x); } });
結果:
さらにテストを追加する予定です。ただし、その前に、ヘルパー関数makeTest
とfor
グループ化する必要があることに注意してください。他のテストではmakeTest
必要ありません。 for
でのみ必要です。共通のタスクは、 pow
指定されたべき乗にどのように累乗するかを確認することです。
グループ化は、ネストされたdescribe
で行われます。
description("pow", function() { description("x の 3 乗", function() { 関数 makeTest(x) { 期待値 = x * x * x; とします。 it(`${x} の 3 乗は ${expected}`, function() { assert.equal(pow(x, 3), Expected); }); } for (x = 1; x <= 5; x++) { makeTest(x); } }); // ... ここでさらにテストを続けます。説明と追加の両方が可能です });
ネストされたdescribe
、テストの新しい「サブグループ」を定義します。出力では、タイトルのインデントが確認できます。
将来的には、 it
さらに追加し、独自のヘルパー関数を使用して最上位にdescribe
ができるようになり、 makeTest
表示されなくなります。
before/after
およびbeforeEach/afterEach
テストの実行前before/after
関数や、毎回のit
前後に実行するbeforeEach/afterEach
関数もセットアップできます。
例えば:
description("テスト", function() { before(() =>alert("テスト開始 – すべてのテスト前")); after(() =>alert("テストが終了しました – すべてのテスト後")); beforeEach(() =>alert("テスト前 – テストに入る")); afterEach(() =>alert("テスト後 – テストを終了")); it('テスト 1', () => アラート(1)); it('テスト 2', () => アラート(2)); });
実行シーケンスは次のようになります。
テスト開始 – すべてのテスト前 (前) テスト前 – テストを入力します (beforeEach) 1 テスト後 – テストを終了します (afterEach) テスト前 – テストを入力します (beforeEach) 2 テスト後 – テストを終了します (afterEach) テスト終了 – すべてのテスト後 (後)
サンドボックスでサンプルを開きます。
通常、 beforeEach/afterEach
およびbefore/after
テスト (またはテスト グループ) 間で初期化、カウンタのゼロ化、またはその他の処理を実行するために使用されます。
pow
の基本的な機能は完了です。開発の最初の反復が完了しました。お祝いをしてシャンパンを飲み終わったら、さらに改善していきましょう。
すでに述べたように、関数pow(x, n)
は正の整数値n
を扱うことを目的としています。
数学的エラーを示すために、JavaScript 関数は通常NaN
を返します。 n
の無効な値についても同じことを行ってみましょう。
まず動作を仕様に追加しましょう(!):
description("pow", function() { // ... it("負の n の場合、結果は NaN になります", function() { assert.isNaN(pow(2, -1)); }); it("非整数 n の場合、結果は NaN になります", function() { assert.isNaN(pow(2, 1.5)); }); });
新しいテストの結果:
新しく追加されたテストは、実装がサポートしていないため失敗します。これが BDD の実行方法です。まず、失敗するテストを作成し、次にそれらの実装を作成します。
その他の主張
アサーションassert.isNaN
に注意してください。これはNaN
をチェックします。
Chai には他にも次のような主張があります。
assert.equal(value1, value2)
– value1 == value2
等しいかどうかをチェックします。
assert.strictEqual(value1, value2)
– 厳密な等価性value1 === value2
をチェックします。
assert.notEqual
、 assert.notStrictEqual
– 上記の逆チェック。
assert.isTrue(value)
– value === true
であることを確認します
assert.isFalse(value)
– value === false
であることを確認します
…完全なリストはドキュメントにあります
したがって、 pow
にいくつかの行を追加する必要があります。
関数 pow(x, n) { if (n < 0) は NaN を返します。 if (Math.round(n) != n) は NaN を返します。 結果 = 1 とします。 for (let i = 0; i < n; i++) { 結果 *= x; } 結果を返します。 }
これで動作し、すべてのテストに合格しました。
サンドボックスで完全な最終例を開きます。
BDD では、仕様が最初にあり、次に実装が続きます。最終的には仕様とコードの両方が完成します。
この仕様は次の 3 つの方法で使用できます。
テストとして – コードが正しく動作することを保証します。
ドキュメントとして – タイトルはdescribe
であり、関数が何を行うかを示しit
。
例として – テストは、関数がどのように使用できるかを示す実際に動作する例です。
この仕様があれば、関数を安全に改善、変更、さらには最初から書き直すことができ、それが引き続き正しく動作することを確認できます。
これは、関数が多くの場所で使用される大規模なプロジェクトで特に重要です。このような関数を変更した場合、その関数を使用するすべての場所が引き続き正しく動作するかどうかを手動で確認する方法はありません。
テストがなければ、人々には次の 2 つの方法があります。
何があっても変化を実行すること。そして、おそらく手動で何かをチェックし損ねるため、ユーザーはバグに遭遇します。
あるいは、テストがないためにエラーに対する罰が厳しい場合、人々はそのような関数を変更することを恐れるようになり、コードが古くなり、誰もコードに手を出したくなくなります。開発には良くありません。
自動テストはこれらの問題を回避するのに役立ちます。
プロジェクトがテストでカバーされている場合、そのような問題は発生しません。変更を加えた後、テストを実行して、数秒以内に行われた多くのチェックを確認できます。
さらに、十分にテストされたコードはより優れたアーキテクチャを備えています。
当然のことながら、自動テストされたコードの方が修正や改善が容易であるためです。しかし、別の理由もあります。
テストを作成するには、すべての関数にタスクが明確に記述され、入出力が明確に定義されるようにコードを編成する必要があります。それは、最初から優れたアーキテクチャを意味します。
現実の生活では、それは時にはそれほど簡単ではありません。実際のコードがどのように動作するかがまだ明確ではないため、実際のコードの前に仕様を記述するのが難しい場合があります。しかし、一般に、テストを作成すると、開発がより速く、より安定します。
チュートリアルの後半では、テストが組み込まれた多くのタスクに遭遇します。より実践的な例が表示されます。
テストを作成するには、JavaScript の十分な知識が必要です。しかし、私たちはそれを学び始めたばかりです。したがって、すべてを落ち着かせるために、現時点ではテストを作成する必要はありませんが、たとえこの章よりも少し複雑であっても、すでにテストを読むことができるはずです。
重要度: 5
以下のpow
のテストで何が間違っているのでしょうか?
it("x の n 乗", function() { x = 5 とします。 結果 = x とします。 assert.equal(pow(x, 1), result); 結果 *= x; assert.equal(pow(x, 2), result); 結果 *= x; assert.equal(pow(x, 3), result); });
PS 構文的にはテストは正しく、合格します。
このテストは、開発者がテストを作成するときに遭遇する誘惑の 1 つを示しています。
ここにあるものは実際には 3 つのテストですが、3 つのアサートを含む 1 つの関数としてレイアウトされています。
この方法で書く方が簡単な場合もありますが、エラーが発生した場合、何が問題だったのかがあまり明確になりません。
複雑な実行フローの途中でエラーが発生した場合は、その時点のデータを把握する必要があります。実際にテストをデバッグする必要があります。
テストを複数のit
ブロックに分割し、入力と出力を明確に記述する方がはるかに優れています。
このような:
description("x の n 乗", function() { it("5 の 1 乗は 5 に等しい", function() { アサート.equal(pow(5, 1), 5); }); it("5 の 2 乗は 25", function() { アサート.equal(pow(5, 2), 25); }); it("5 の 3 乗は 125", function() { アサート.equal(pow(5, 3), 125); }); });
単一のit
describe
とit
ブロックのグループに置き換えました。これで、何かが失敗した場合でも、データが何であったかを明確に確認できるようになります。
また、 it
の代わりにit.only
と書くことで、単一のテストを分離し、スタンドアロン モードで実行することもできます。
description("x の n 乗", function() { it("5 の 1 乗は 5 に等しい", function() { アサート.equal(pow(5, 1), 5); }); // Mocha はこのブロックのみを実行します it.only("5 の 2 乗は 25 に等しい", function() { アサート.equal(pow(5, 2), 25); }); it("5 の 3 乗は 125", function() { アサート.equal(pow(5, 3), 125); }); });