How to mock JavaScript interop objects
In this tutorial, you'll learn how to mock JS objects so that you can test interop instance members without having to use a real implementation.
Background and motivation
#Mocking classes in Dart is usually done through overriding instance members. However, since extension types are used to declare interop types, all extension type members are dispatched statically and therefore overriding can't be used. This limitation is true for extension members as well, and therefore instance extension type or extension members can't be mocked.
While this applies to any non-external
extension type member, external
interop members are special as they invoke members on a JS value.
extension type Date(JSObject _) implements JSObject {
external int getDay();
}
As discussed in the Usage section, calling getDay()
will result in calling
getDay()
on the JS object. Therefore, by using a different JSObject
, a
different implementation of getDay
can be called.
In order to do this, there should be some mechanism of creating a JS object that
has a property getDay
which when called, calls a Dart function. A simple way
is to create a JS object and set the property getDay
to a converted callback
e.g.
final date = Date(JSObject());
date['getDay'] = (() => 0).toJS;
While this works, this is prone to error and doesn't scale well when you are
using many interop members. It also doesn't handle getters or setters properly.
Instead, you should use a combination of createJSInteropWrapper
and
@JSExport
to declare a type that provides an implementation for all the
external
instance members.
Mocking example
#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
allows you to declare a class that can be used in
createJSInteropWrapper
. createJSInteropWrapper
will create an object literal
that maps each of the class' instance member names (or renames) to a JS
callback, which is created using Function.toJS
. When called, the JS callback
will in turn call the instance member. In the above example, getting and setting
counter.value
gets and sets fakeCounter.value
.
You can specify only some members of a class to be exported by omitting the
annotation from the class and instead only annotate the specific members. You
can see more specifics on more specialized exporting (including inheritance) in
the documentation of @JSExport
.
除非另有说明,文档之所提及适用于 Dart 3.5.3 版本,本页面最后更新时间: 2024-08-02。 查看文档源码 或者 报告页面问题。