メインコンテンツにスキップ

JavaScriptInteropオブジェクトのモック方法

このチュートリアルでは、実際のインプリメンテーションを使用せずに、InteropインスタンスメンバーをテストできるようにJSオブジェクトをモックする方法を学びます。

背景と動機

#

Dartでクラスをモックすることは、通常、インスタンスメンバーをオーバーライドすることによって行われます。しかし、拡張機能型はInterop型を宣言するために使用されるため、すべての拡張機能型メンバーは静的にディスパッチされ、したがってオーバーライドは使用できません。この制限は拡張機能メンバーにも当てはまります。そのため、インスタンス拡張機能型または拡張機能メンバーはモックできません。

これは、externalではない拡張機能型メンバーに適用されますが、externalInteropメンバーは、JS値のメンバーを呼び出すため、特別です。

dart
extension type Date(JSObject _) implements JSObject {
  external int getDay();
}

使用方法」セクションで説明したように、getDay()を呼び出すと、JSオブジェクトのgetDay()が呼び出されます。したがって、別のJSObjectを使用することで、getDayの別のインプリメンテーションを呼び出すことができます。

これを行うには、getDayプロパティを持ち、呼び出されたときにDart関数を呼び出すJSオブジェクトを作成するメカニズムが必要です。簡単な方法は、JSオブジェクトを作成し、getDayプロパティを変換されたコールバックに設定することです。たとえば、

dart
final date = Date(JSObject());
date['getDay'] = (() => 0).toJS;

これは機能しますが、エラーが発生しやすく、多くのInteropメンバーを使用している場合はうまくスケールしません。また、ゲッターやセッターも適切に処理しません。代わりに、createJSInteropWrapper@JSExportの組み合わせを使用して、すべてのexternalインスタンスメンバーのインプリメンテーションを提供する型を宣言する必要があります。

モックの例

#
dart
import 'dart:js_interop';

import 'package:expect/minitest.dart';

// The Dart class must have `@JSExport` on it or at least one of its instance
// members.
@JSExport()
class FakeCounter {
  int value = 0;
  @JSExport('increment')
  void renamedIncrement() {
    value++;
  }
  void decrement() {
    value--;
  }
}

extension type Counter(JSObject _) implements JSObject {
  external int value;
  external void increment();
  void decrement() {
    value -= 2;
  }
}

void main() {
  var fakeCounter = FakeCounter();
  // Returns a JS object whose properties call the relevant instance members in
  // `fakeCounter`.
  var counter = createJSInteropWrapper<FakeCounter>(fakeCounter) as Counter;
  // Calls `FakeCounter.value`.
  expect(counter.value, 0);
  // `FakeCounter.renamedIncrement` is renamed to `increment`, so it gets
  // called.
  counter.increment();
  expect(counter.value, 1);
  expect(fakeCounter.value, 1);
   // Changes in the fake affect the wrapper and vice-versa.
  fakeCounter.value = 0;
  expect(counter.value, 0);
  counter.decrement();
  // Because `Counter.decrement` is non-`external`, we never called
  // `FakeCounter.decrement`.
  expect(counter.value, -2);
}

@JSExportを使用すると、createJSInteropWrapperで使用できるクラスを宣言できます。createJSInteropWrapperは、クラスのインスタンスメンバー名(または名前変更)を、Function.toJSを使用して作成されたJSコールバックにマッピングするオブジェクトリテラルを作成します。呼び出されたとき、JSコールバックはインスタンスメンバーを呼び出します。上記の例では、counter.valueの取得と設定は、fakeCounter.valueの取得と設定を行います。

アノテーションをクラスから省略し、特定のメンバーのみをアノテーションすることで、クラスの一部のメンバーのみをエクスポートするように指定できます。継承を含む、より高度なエクスポートの詳細については、@JSExportのドキュメントで確認できます。

このメカニズムは、テスト専用ではないことに注意してください。これを使用して、任意のDartオブジェクトにJSインターフェイスを提供できます。これにより、Dartオブジェクトを定義済みのインターフェイスで、実質的にJSにエクスポートできます。