自動テストは今後のタスクで使用され、実際のプロジェクトでも広く使用されています。
なぜテストが必要なのでしょうか?
関数を書くとき、私たちは通常、それが何をすべきかを想像することができます。どのパラメータがどの結果をもたらすか。
開発中、私たちは関数を実行し、結果を期待されるものと比較することで関数をチェックできます。たとえば、コンソールでそれを行うことができます。
何か問題がある場合は、コードを修正し、再度実行して結果を確認し、それが動作するまで続けます。
しかし、このような手動の「再実行」は不完全です。
手動での再実行によるコードのテストでは、何かを見逃しがちです。
たとえば、関数 f
を作成しているとします。コードを書いてテストします。 f(1)
は動作しますが、 f(2)
は動作しません。コードを修正すると、 f(2)
は動作するようになりました。これで完了でしょうか?しかし、 f(1)
を再テストすることを忘れました。それはエラーにつながる可能性があります。
それは非常に典型的なことです。何かを開発するとき、私たちは多くの可能性のあるユースケースを念頭に置いています。しかし、プログラマーが変更のたびにそれらすべてを手動でチェックすることを期待するのは難しいです。したがって、あるものを修正して別のものを壊すことが容易になります。
自動テストとは、コードに加えて、テストが個別に記述されることを意味します。それらはさまざまな方法で私たちの関数を実行し、結果を期待されるものと比較します。
振る舞い駆動開発 (BDD)
振る舞い駆動開発、略してBDDという手法から始めましょう。
BDDは、テスト、ドキュメント、例の3つの要素が1つになったものです。
BDDを理解するために、実践的な開発の事例を検討します。
「pow」の開発:仕様
整数べき乗 n
に x
を累乗する関数 pow(x, n)
を作成したいとしましょう。 n≥0
と仮定します。
そのタスクは単なる例です。JavaScriptにはそれを行うことができる **
演算子がありますが、ここでは、より複雑なタスクにも適用できる開発フローに焦点を当てます。
pow
のコードを作成する前に、関数が何をするべきかを想像し、それを記述することができます。
このような記述は、仕様、略して仕様と呼ばれ、次のようなユースケースの説明とそれらのテストが含まれています。
describe("pow", function() {
it("raises to n-th power", function() {
assert.equal(pow(2, 3), 8);
});
});
仕様には、上記に見られる3つの主要な構成要素があります
describe("title", function() { ... })
-
私たちはどのような機能を記述していますか?このケースでは、関数
pow
を記述しています。it
ブロックである「ワーカー」をグループ化するために使用されます。 it("ユースケースの説明", 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
などの共通のテスト関数、およびテストを実行するメイン関数を提供します。 - Chai - 多くのアサーションを備えたライブラリ。多くの異なるアサーションを使用できますが、今のところは
assert.equal
のみが必要です。 - Sinon - 関数をスパイしたり、組み込み関数をエミュレートしたりするためのライブラリ。後で必要になります。
これらのライブラリは、ブラウザ内テストとサーバーサイドテストの両方に適しています。ここでは、ブラウザのバリアントを検討します。
これらのフレームワークと pow
仕様を含む完全なHTMLページ
<!DOCTYPE html>
<html>
<head>
<!-- add mocha css, to show results -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
<!-- add mocha framework code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
<script>
mocha.setup('bdd'); // minimal setup
</script>
<!-- add chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
<script>
// chai has a lot of stuff, let's make assert global
let assert = chai.assert;
</script>
</head>
<body>
<script>
function pow(x, n) {
/* function code is to be written, empty now */
}
</script>
<!-- the script with tests (describe, it...) -->
<script src="test.js"></script>
<!-- the element with id="mocha" will contain test results -->
<div id="mocha"></div>
<!-- run tests! -->
<script>
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
の簡単な実装を行いましょう
function pow(x, n) {
return 8; // :) we cheat!
}
やった、これで動いた!
仕様の改善
私たちが行ったことは間違いなく不正行為です。関数は機能しません。 pow(3,4)
を計算しようとすると正しくない結果になりますが、テストは合格します。
...しかし、状況は非常に典型的で、実際にはよく起こります。テストは合格しますが、関数は誤って動作します。私たちの仕様は不完全です。それにもっとユースケースを追加する必要があります。
pow(3, 4) = 81
であることを確認するために、もう1つのテストを追加しましょう。
ここで、テストを編成する2つの方法のいずれかを選択できます
-
最初のバリアント - 同じ
it
にもう1つのassert
を追加しますdescribe("pow", function() { it("raises to n-th power", function() { assert.equal(pow(2, 3), 8); assert.equal(pow(3, 4), 81); }); });
-
2番目 - 2つのテストを行います
describe("pow", function() { it("2 raised to power 3 is 8", function() { assert.equal(pow(2, 3), 8); }); it("3 raised to power 4 is 81", function() { assert.equal(pow(3, 4), 81); }); });
主な違いは、 assert
がエラーをトリガーすると、 it
ブロックがすぐに終了することです。したがって、最初のバリアントでは、最初の assert
が失敗した場合、2番目の assert
の結果は表示されません。
テストを分離すると、何が起こっているかについての詳細な情報を取得するのに役立つため、2番目のバリアントの方が優れています。
それに加えて、従うべきもう1つのルールがあります。
1つのテストは1つのことをチェックします。
テストを見て、2つの独立したチェックがある場合は、それを2つのより単純なチェックに分割する方が良いです。
それで、2番目のバリアントを続けましょう。
結果
予想どおり、2番目のテストは失敗しました。確かに、私たちの関数は常に 8
を返しますが、 assert
は 81
を期待しています。
実装の改善
テストに合格するためのより現実的なものを書きましょう
function pow(x, n) {
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
関数が正常に機能することを確認するために、より多くの値でテストしましょう。 it
ブロックを手動で書き込む代わりに、 for
でそれらを生成できます
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
結果
ネストされた describe
さらにテストを追加します。しかし、その前に、ヘルパー関数 makeTest
と for
を一緒にグループ化する必要があることに注意しましょう。他のテストでは makeTest
は必要ありません。 for
でのみ必要です。それらの共通のタスクは、 pow
が与えられたべき乗にどのように上昇するかを確認することです。
グループ化は、ネストされた describe
で行われます
describe("pow", function() {
describe("raises x to power 3", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} in the power 3 is ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
for (let x = 1; x <= 5; x++) {
makeTest(x);
}
});
// ... more tests to follow here, both describe and it can be added
});
ネストされたdescribe
は、テストの新しい「サブグループ」を定義します。出力では、タイトル付きのインデントを確認できます。
今後、トップレベルに独自のヘルパー関数を持つit
やdescribe
をさらに追加できますが、それらはmakeTest
を参照しません。
before/after
とbeforeEach/afterEach
テストの実行前/後に実行するbefore/after
関数と、すべてのit
の実行前/後に実行するbeforeEach/afterEach
関数を設定できます。
例:
describe("test", function() {
before(() => alert("Testing started – before all tests"));
after(() => alert("Testing finished – after all tests"));
beforeEach(() => alert("Before a test – enter a test"));
afterEach(() => alert("After a test – exit a test"));
it('test 1', () => alert(1));
it('test 2', () => alert(2));
});
実行順序は次のようになります。
Testing started – before all tests (before)
Before a test – enter a test (beforeEach)
1
After a test – exit a test (afterEach)
Before a test – enter a test (beforeEach)
2
After a test – exit a test (afterEach)
Testing finished – after all tests (after)
通常、beforeEach/afterEach
とbefore/after
は、テスト(またはテストグループ)間で初期化、カウンターのリセット、またはその他の処理を実行するために使用されます。
仕様の拡張
pow
の基本機能は完了しました。開発の最初のイテレーションは完了です。お祝いをしてシャンパンを飲んだら、次に進んで改善しましょう。
前述のように、関数pow(x, n)
は正の整数値n
で動作するように設計されています。
数学的なエラーを示すために、JavaScript関数は通常NaN
を返します。無効な値n
に対しても同じことを行いましょう。
最初に、この動作を仕様に追加しましょう(!)
describe("pow", function() {
// ...
it("for negative n the result is NaN", function() {
assert.isNaN(pow(2, -1));
});
it("for non-integer n the result is 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
に数行追加する必要があります。
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;
let result = 1;
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
これで動作し、すべてのテストが合格します。
まとめ
BDDでは、最初に仕様が記述され、次に実装が続きます。最後に、仕様とコードの両方が揃います。
仕様は3つの方法で使用できます。
- テストとして – コードが正しく動作することを保証します。
- ドキュメントとして –
describe
とit
のタイトルは、関数が何をするかを説明します。 - 例として – テストは、実際に関数がどのように使用できるかを示す動作例です。
仕様があれば、関数を安全に改善、変更、さらには最初から書き直すことができ、それでも正しく動作することを確認できます。
これは、関数が多くの場所で使用されている大規模なプロジェクトで特に重要です。このような関数を変更する場合、それを使用するすべての場所がまだ正しく動作しているかどうかを手動で確認する方法はありません。
テストがない場合、人々には2つの方法があります。
- とにかく変更を実行する。そして、手動で何かをチェックし損なう可能性があるため、ユーザーはバグに遭遇します。
- または、エラーに対する罰則が厳しい場合、テストがないため、人々はそのような関数を変更することを恐れるようになり、コードは古くなり、誰も関与したくなくなります。開発には良くありません。
自動テストはこれらの問題を回避するのに役立ちます!
プロジェクトがテストでカバーされている場合、そのような問題はありません。変更後、テストを実行すると、数秒で多数のチェックが行われることがわかります。
さらに、十分にテストされたコードは、より優れたアーキテクチャを備えています。
当然、自動テストされたコードは変更や改善が容易であるためです。しかし、もう1つの理由もあります。
テストを記述するには、すべての関数が明確に記述されたタスク、明確に定義された入力と出力を持つようにコードを編成する必要があります。つまり、最初から適切なアーキテクチャがあることを意味します。
現実には、それは時々それほど簡単ではありません。実際のコードの前に仕様を書くのが難しい場合があります。なぜなら、それがどのように動作するべきかまだ明確でないからです。しかし、一般的にテストを書くと、開発がより速く、より安定します。
このチュートリアルの後半では、テストが組み込まれた多くのタスクに出会うでしょう。そのため、より実践的な例を見るでしょう。
テストを記述するには、優れたJavaScriptの知識が必要です。しかし、私たちはそれを学び始めたばかりです。したがって、すべてを落ち着かせるために、現時点ではテストを記述する必要はありませんが、この章よりも少し複雑な場合でも、それらを読み取ることができるはずです。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグでラップし、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。