単体テスト

コードを変更または追加したら、既存の単体テストを実行し、さらにテストを作成することを検討する必要があります。すべてのテストは、圧縮されていないバージョンのコードに対して実行されます。

単体テストには、JS テストとブロック生成ツールのテストがあります。

JS テスト

JS テストでは、Blockly のコア内の内部 JavaScript 関数の動作を確認します。単体テストの実行には Mocha、依存関係のスタブには Sinon、コードに関するアサーションの作成には Chai を使用します。

テストの実行

blockly と blockly-samples の両方で、npm run test が単体テストを実行します。Blockly では、リンティングやコンパイルなどの他のテストも実行されます。ブラウザで tests/mocha/index.html を開いて、すべての mocha テストをインタラクティブに実行することもできます。

テストの作成

Mocha TDD インターフェースを使用してテストを実行します。テストはスイートに編成されます。スイートには、追加のサブスイートやテストの両方を含めることができます。通常、Blockly の各コンポーネント(toolboxworkspace など)には、1 つ以上のスイートを含む独自のテストファイルがあります。各スイートには、そのスイートの各テストの前に呼び出され、後に呼び出される setup メソッドと teardown メソッドを設定できます。

テストヘルパー

Blockly には、テストの作成時に役立つ Blockly 固有のヘルパー関数がいくつかあります。これらは coreblockly-samples にあります。

ヘルパー関数には sharedTestSetupsharedTestTeardown があり、テストの前後に呼び出す必要があります(要件のセクションを参照)。

sharedTestSetup:
  • sinon の偽のタイマーを設定します(一部のテストでは this.clock.runAll を使用する必要があります)。
  • Blockly.Events.fire を直ちに起動するようにスタブします(構成可能)。
  • defineBlocksWithJsonArray で定義された blockType の自動クリーンアップを設定します。
  • this コンテキストで、アクセス可能なプロパティをいくつか宣言します。
    • this.clock(ただし、復元しないでください。復元すると sharedTestTeardown で問題が発生します)
    • this.eventsFireStub
    • this.sharedCleanupaddMessageToCleanupaddBlockTypeToCleanup で使用)(注: defineBlocksWithJsonArray を使用してブロックを定義した場合は、addBlockTypeToCleanup を使用する必要はありません)

この関数には、設定を構成する省略可能な options パラメータが 1 つあります。現在、Blockly.Events.fire をスタブしてすぐに発行するかどうかを決定するためにのみ使用されます(デフォルトではスタブされます)。

sharedTestTeardown:
  • ワークスペース this.workspace を破棄します(定義された場所に応じて、詳細についてはテスト要件のセクションをご覧ください)。
  • すべてのスタブを復元します。
  • defineBlocksWithJsonArrayaddBlockTypeToCleanup で追加されたすべてのブロックタイプをクリーンアップします。
  • addMessageToCleanup を通じて追加されたすべてのメッセージをクリーンアップします。

テストの要件

  • 各テストは、最も外側のスイートのセットアップの最初の行として sharedTestSetup.call(this); を呼び出し、ファイルの最も外側のスイートのテイクダウンの最後の行として sharedTestTeardown.call(this); を呼び出す必要があります。
  • 汎用ツールボックスを含むワークスペースが必要な場合は、テスト index.html事前設定されたツールボックスのいずれかを使用できます。以下に例を示します。
  • this.workspace は適切に廃棄する必要があります。ほとんどのテストでは、最も外側のスイートで this.workspace を定義し、後続のすべてのテストでそれを使用しますが、内部スイートで定義または再定義する場合もあります(たとえば、テストの一つに、最初に設定したオプションとは異なるオプションを持つワークスペースが必要です)。テスト終了時に廃棄する必要があります。
    • 最も外側のスイートで this.workspace を定義し、それを再定義しない場合は、それ以上のアクションは必要ありません。sharedTestTeardown によって自動的に破棄されます。
    • this.workspace を内部スイートで初めて定義する場合(つまり、最も外側のスイートで定義しなかった場合)、そのスイートのテイクダウンで workspaceTeardown.call(this, this.workspace) を呼び出して、手動で破棄する必要があります。
    • 最も外側のスイートで this.workpace を定義し、内部テストスイートで再定義する場合は、まず workspaceTeardown.call(this, this.workspace) を呼び出して再定義する前に、最上位スイートで定義された元のワークスペースを破棄する必要があります。また、この内部スイートのテイクダウンで workspaceTeardown.call(this, this.workspace) を再度呼び出して、新しい値を手動で破棄する必要があります。

テストの構造

単体テストは通常、一連の構造に従います。これは、準備、実行、アサートと要約できます。

  1. 準備: テスト対象の動作に必要な状態と条件を設定します。
  2. Act: テスト対象のコードを呼び出して、テスト対象の動作をトリガーします。
  3. Assert: 戻り値またはモックされたオブジェクトとのやり取りについてアサートを行い、正確性を確認します。

単純なテストでは、設定する動作がない場合があります。その場合は、テスト対象のコードへの呼び出しをアサーションにインライン化することで、実行ステージとアサート ステージを組み合わせることができます。より複雑なケースでは、これらの 3 つのステージに沿ってテストを作成すると、テストの読みやすさが向上します。

テストファイルの例を次に示します(実際のファイルから簡素化しています)。

suite('Flyout', function() {
  setup(function() {
    sharedTestSetup.call(this);
    this.toolboxXml = document.getElementById('toolbox-simple');
    this.workspace = Blockly.inject('blocklyDiv',
        {
          toolbox: this.toolboxXml
        });
  });

  teardown(function() {
    sharedTestTeardown.call(this);
  });

  suite('simple flyout', function() {
    setup(function() {
      this.flyout = this.workspace.getFlyout();
    });
    test('y is always 0', function() {
      // Act and assert stages combined for simple test case
      chai.assert.equal(this.flyout.getY(), 0, 'y coordinate in vertical flyout is 0');
    });
    test('x is right of workspace if flyout at right', function() {
      // Arrange
      sinon.stub(this.flyout.targetWorkspace, 'getMetrics').returns({
        viewWidth: 100,
      });
      this.flyout.targetWorkspace.toolboxPosition = Blockly.TOOLBOX_AT_RIGHT;
      this.flyout.toolboxPosition_ = Blockly.TOOLBOX_AT_RIGHT;

      // Act
      var x = this.flyout.getX();

      // Assert
      chai.assert.equal(x, 100, 'x is right of workspace');
    });
  });
});

この例の注意点は次のとおりです。

  • スイートには、追加の setup メソッドと teardown メソッドを持つ他のスイートを含めることができます。
  • 各スイートとテストにはわかりやすい名前を付けます。
  • Chai アサーションを使用して、コードに関するアサーションを行います。
    • テストが失敗した場合に表示される文字列引数を指定できます。これにより、エラーのあるテストを簡単にデバッグできます。
    • パラメータの順序は chai.assert.equal(actualValue, expectedValue, optionalMessage) です。actualexpected を入れ替えると、エラー メッセージが意味不明になります。
  • Sinon は、実際のコードを呼び出したくない場合にメソッドをスタブするために使用されます。この例では、実際の指標関数を呼び出す必要はありません。このテストには関連性がないためです。テスト対象のメソッドで結果がどのように使用されるかが重要です。Sinon は getMetrics 関数をスタブして、テストの断言で簡単に確認できる返信を返します。
  • 各スイートの setup メソッドには、すべてのテストに適用される一般的な設定のみを含める必要があります。特定の動作のテストが特定の条件に依存している場合は、その条件を関連するテストで明確に述べておく必要があります。

テストのデバッグ

  • ブラウザでテストを開き、デベロッパー ツールを使用してブレークポイントを設定し、テストが予期せず失敗している(または予期せず成功している)かどうかを調査できます。
  • テストまたはスイートの .only() または .skip() を設定して、そのテストセットのみを実行するか、テストをスキップします。例:

    suite.only('Workspace', function () {
      suite('updateToolbox', function () {
        test('test name', function () {
          // ...
        });
        test.skip('test I don’t care about', function () {
          // ...
        });
      });
    });
    

    コードを commit する前に、忘れずに削除してください。

ブロック生成ツールのテスト

各ブロックには独自の単体テストがあります。これらのテストでは、ブロックが意図したとおりに関数よりもコードを生成するかどうかを確認します。

  1. Firefox または Safari で tests/generators/index.html を読み込みます。Chrome と Opera には、ローカルの「file://」システムからテストを読み込むことを禁止するセキュリティ制限があります(問題 41024 および 47416)。
  2. プルダウン メニューからテストするシステムの関連部分を選択し、[読み込み] をクリックします。ワークスペースにブロックが表示されます。
  3. [JavaScript] をクリックします。
    生成されたコードをコピーして、JavaScript コンソールで実行します。出力が「OK」で終わる場合、テストは合格です。
  4. [Python] をクリックします。
    生成されたコードをコピーして、Python インタープリタで実行します。出力が「OK」で終わっている場合は、テストに合格しています。
  5. [PHP] をクリックします。
    生成されたコードをコピーして、PHP インタープリタで実行します。出力が「OK」で終わっている場合は、テストに合格しています。
  6. [Lua] をクリックします。
    生成されたコードをコピーして、Lua インタープリタで実行します。出力が「OK」で終了している場合、テストは合格です。
  7. [Dart] をクリックします。
    生成されたコードをコピーして、Dart インタープリタで実行します。出力が「OK」で終わっている場合は、テストに合格しています。

ブロック生成ツールのテスト編集

  1. ブラウザで tests/generators/index.html を読み込みます。
  2. プルダウン メニューからシステムの関連部分を選択し、[読み込み] をクリックします。ワークスペースにブロックが表示されます。
  3. ブロックに変更を加え、必要に応じて追加します。
  4. [XML] をクリックします。
  5. 生成された XML を tests/generators/ の適切なファイルにコピーします。