単体テスト

コードを変更または追加したら、既存の単体テストを実行して、さらに記述することを検討してください。すべてのテストは、非圧縮バージョンのコードで実行されます。

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

JS テスト

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

テストの実行

ブロックサンプルとブロックサンプルの両方で、npm run test が単体テストを実行します。ブロック内で、lint チェックやコンパイルなどの他のテストも実行されます。ブラウザで tests/mocha/index.html を開いて、すべての Mocha テストをインタラクティブに実行することもできます。

テストの作成

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

テストヘルパー

テストを作成する際に役立つ Blockly 固有のヘルパー関数が多数用意されています。これらはコアブロックサンプルにあります。

ヘルパー関数には sharedTestSetupsharedTestTeardown があります。これらはテストの前後に呼び出すことが必須です(「要件」セクションをご覧ください)。

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

この関数には、設定を構成するオプションの options パラメータが 1 つあります。現在は、Blockly.Events.fire をすぐに起動するようにスタブするかどうか(デフォルトではスタブ)を判断するためにのみ使用されます。

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

テストの要件

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

テストの構成

単体テストは通常、配置、作用、アサートというように要約できるセット構造に従います。

  1. Arrange: 世界の状態と、テスト対象の動作に必要な条件をセットアップします。
  2. Act: テスト対象のコードを呼び出して、テスト対象の動作をトリガーします。
  3. Assert: 正確性を検証するために、戻り値やモックされたオブジェクトとのインタラクションに関するアサーションを作成します。

単純なテストでは、調整する動作がない場合があります。act ステージと 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://」システムからテストを読み込めません(問題 4102447416)。
  2. テストするシステムの関連する部分をプルダウン メニューから選択し、[Load] をクリックします。ワークスペースにブロックが表示されます。
  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. システムの関連する部分をプルダウン メニューから選択し、[Load] をクリックします。ワークスペースにブロックが表示されます。
  3. ブロックを変更または追加します。
  4. [XML] をクリックします。
  5. 生成された XML を tests/generators/ の適切なファイルにコピーします。