2022年10月14日

Mocha を使用した自動テスト

自動テストは今後のタスクで使用され、実際のプロジェクトでも広く使用されています。

なぜテストが必要なのでしょうか?

関数を書くとき、私たちは通常、それが何をすべきかを想像することができます。どのパラメータがどの結果をもたらすか。

開発中、私たちは関数を実行し、結果を期待されるものと比較することで関数をチェックできます。たとえば、コンソールでそれを行うことができます。

何か問題がある場合は、コードを修正し、再度実行して結果を確認し、それが動作するまで続けます。

しかし、このような手動の「再実行」は不完全です。

手動での再実行によるコードのテストでは、何かを見逃しがちです。

たとえば、関数 f を作成しているとします。コードを書いてテストします。 f(1) は動作しますが、 f(2) は動作しません。コードを修正すると、 f(2) は動作するようになりました。これで完了でしょうか?しかし、 f(1) を再テストすることを忘れました。それはエラーにつながる可能性があります。

それは非常に典型的なことです。何かを開発するとき、私たちは多くの可能性のあるユースケースを念頭に置いています。しかし、プログラマーが変更のたびにそれらすべてを手動でチェックすることを期待するのは難しいです。したがって、あるものを修正して別のものを壊すことが容易になります。

自動テストとは、コードに加えて、テストが個別に記述されることを意味します。それらはさまざまな方法で私たちの関数を実行し、結果を期待されるものと比較します。

振る舞い駆動開発 (BDD)

振る舞い駆動開発、略してBDDという手法から始めましょう。

BDDは、テスト、ドキュメント、例の3つの要素が1つになったものです。

BDDを理解するために、実践的な開発の事例を検討します。

「pow」の開発:仕様

整数べき乗 nx を累乗する関数 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 ブロックで指定されたテストを実行します。後でそれを見ます。

開発フロー

開発の流れは通常次のようになります

  1. 最も基本的な機能のテストを含む、初期の仕様が作成されます。
  2. 初期の実装が作成されます。
  3. それが動作するかどうかを確認するために、仕様を実行するテストフレームワークMocha(詳細は後述)を実行します。機能が完成していない間、エラーが表示されます。すべてが動作するまで修正を行います。
  4. これで、テストを含む動作する初期実装ができました。
  5. 実装ではまだサポートされていない可能性のあるユースケースを仕様に追加します。テストが失敗し始めます。
  6. 3に進み、テストでエラーが発生しなくなるまで実装を更新します。
  7. 機能が完成するまで、ステップ3〜6を繰り返します。

したがって、開発は反復的です。仕様を書き、それを実装し、テストに合格することを確認し、さらにテストを書き、それらが動作することを確認します。最後に、動作する実装とそのためのテストの両方があります。

この開発フローを実践的なケースで見てみましょう。

最初のステップはすでに完了しています。 pow の初期仕様があります。次に、実装を作成する前に、いくつかのJavaScriptライブラリを使用してテストを実行し、それらが動作していることを確認します(それらはすべて失敗します)。

実際の仕様

このチュートリアルでは、テストに次のJavaScriptライブラリを使用します

  • Mocha - コアフレームワーク:describeitなどの共通のテスト関数、およびテストを実行するメイン関数を提供します。
  • 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つの部分に分けることができます

  1. <head> - テスト用のサードパーティライブラリとスタイルを追加します。
  2. テストする関数を持つ <script>。このケースでは、 pow のコードを使用します。
  3. テスト - このケースでは、上記の describe("pow", ...) を持つ外部スクリプト test.js です。
  4. HTML要素 <div id="mocha"> は、Mochaが結果を出力するために使用します。
  5. テストはコマンド 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つの方法のいずれかを選択できます

  1. 最初のバリアント - 同じ 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番目 - 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 を返しますが、 assert81 を期待しています。

実装の改善

テストに合格するためのより現実的なものを書きましょう

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

さらにテストを追加します。しかし、その前に、ヘルパー関数 makeTestfor を一緒にグループ化する必要があることに注意しましょう。他のテストでは 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は、テストの新しい「サブグループ」を定義します。出力では、タイトル付きのインデントを確認できます。

今後、トップレベルに独自のヘルパー関数を持つitdescribeをさらに追加できますが、それらはmakeTestを参照しません。

before/afterbeforeEach/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/afterEachbefore/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.notEqualassert.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つの方法で使用できます。

  1. テストとして – コードが正しく動作することを保証します。
  2. ドキュメントとして – describeitのタイトルは、関数が何をするかを説明します。
  3. として – テストは、実際に関数がどのように使用できるかを示す動作例です。

仕様があれば、関数を安全に改善、変更、さらには最初から書き直すことができ、それでも正しく動作することを確認できます。

これは、関数が多くの場所で使用されている大規模なプロジェクトで特に重要です。このような関数を変更する場合、それを使用するすべての場所がまだ正しく動作しているかどうかを手動で確認する方法はありません。

テストがない場合、人々には2つの方法があります。

  1. とにかく変更を実行する。そして、手動で何かをチェックし損なう可能性があるため、ユーザーはバグに遭遇します。
  2. または、エラーに対する罰則が厳しい場合、テストがないため、人々はそのような関数を変更することを恐れるようになり、コードは古くなり、誰も関与したくなくなります。開発には良くありません。

自動テストはこれらの問題を回避するのに役立ちます!

プロジェクトがテストでカバーされている場合、そのような問題はありません。変更後、テストを実行すると、数秒で多数のチェックが行われることがわかります。

さらに、十分にテストされたコードは、より優れたアーキテクチャを備えています。

当然、自動テストされたコードは変更や改善が容易であるためです。しかし、もう1つの理由もあります。

テストを記述するには、すべての関数が明確に記述されたタスク、明確に定義された入力と出力を持つようにコードを編成する必要があります。つまり、最初から適切なアーキテクチャがあることを意味します。

現実には、それは時々それほど簡単ではありません。実際のコードの前に仕様を書くのが難しい場合があります。なぜなら、それがどのように動作するべきかまだ明確でないからです。しかし、一般的にテストを書くと、開発がより速く、より安定します。

このチュートリアルの後半では、テストが組み込まれた多くのタスクに出会うでしょう。そのため、より実践的な例を見るでしょう。

テストを記述するには、優れたJavaScriptの知識が必要です。しかし、私たちはそれを学び始めたばかりです。したがって、すべてを落ち着かせるために、現時点ではテストを記述する必要はありませんが、この章よりも少し複雑な場合でも、それらを読み取ることができるはずです。

タスク

重要度: 5

以下のpowのテストの何が間違っていますか?

it("Raises x to the power n", function() {
  let x = 5;

  let result = x;
  assert.equal(pow(x, 1), result);

  result *= x;
  assert.equal(pow(x, 2), result);

  result *= x;
  assert.equal(pow(x, 3), result);
});

追伸:構文的にはテストは正しく、合格します。

このテストは、開発者がテストを書くときに遭遇する誘惑の1つを示しています。

ここにあるのは実際には3つのテストですが、3つのアサートを持つ単一の関数として配置されています。

このように書く方が簡単な場合もありますが、エラーが発生した場合、何が間違っているのかがはるかにわかりにくくなります。

複雑な実行フローの途中でエラーが発生した場合、その時点でのデータを把握する必要があります。実際には、テストをデバッグする必要があります。

明確に記述された入力と出力を備えた複数のitブロックにテストを分割する方がはるかに優れています。

こんな感じ

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  it("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

単一のitdescribeitブロックのグループに置き換えました。これで、何か失敗した場合、データが何であったかを明確に確認できます。

また、itの代わりにit.onlyと記述することで、単一のテストを分離してスタンドアロンモードで実行できます。

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  // Mocha will run only this block
  it.only("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});
チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善するための提案がある場合は、コメントする代わりにGitHub issueまたはプルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入するには、<code>タグを使用し、複数行の場合は<pre>タグでラップし、10行を超える場合はサンドボックス(plnkrjsbincodepen…)を使用してください。