在 Flutter 测试中模拟一个 Widget
Mock a Widget in Flutter tests
我正在尝试为我的 Flutter 应用程序创建测试。简单示例:
class MyWidget extends StatelessWidget {
@override
build(BuildContext context) {
return MySecondWidget();
}
}
我想验证 MyWidget
实际上是在调用 MySecondWidget
而不构建 MySecondWidget
。
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
在我的例子中,这将不起作用,因为 MySecondWidget
需要一些特定且复杂的设置(例如 API 键,Provider
中的值...)。我想要的是 "mock" MySecondWidget
是一个空的 Container
(例如),这样它在测试期间不会引发任何错误。
我该怎么做?
您可以模拟 MySecondWidget
(例如使用 Mockito),但您确实需要更改您的真实代码以在测试模式下创建 MockMySecondWidget
,因此它并不漂亮。 Flutter 不支持基于 Type
的对象实例化(通过 dart:mirrors
除外,但这与 Flutter 不兼容),因此您不能 'inject' 该类型作为依赖项。要确定您是否处于测试模式,请使用 Platform.environment.containsKey('FLUTTER_TEST')
- 最好在启动时确定一次并将结果设置为全局 final
变量,这将使任何条件语句快速。
一种方法是将子小部件包装到一个函数中,并将该函数传递给父小部件的构造函数:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
@override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
然后你可以制作你的模拟小部件,通过 buildMySecondWidgetFn
测试。
没有开箱即用的东西来模拟小部件。我打算写一些关于如何在测试期间“模拟”/替换小部件的 examples/ideas(例如 SizedBox.shrink()
.
但首先,让我解释一下为什么我认为这不是一个好主意。
在 Flutter 中,您正在构建一个小部件树。一个特定的widget有一个parent,通常有一个或几个children.
Flutter 出于性能原因选择了单通道布局算法(参见this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
据此,我们需要了解 parent 需要它的 children 来构建以获得它们的大小,然后正确地渲染自己。如果您删除它的 children,它的行为可能会完全不同。
如果可能,最好模拟服务。例如,如果您的 child 发出 HTTP 请求,您可以 mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
如果 child 需要特定的提供者,您可以提供一个虚拟的提供者:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
如果您在测试期间仍想用另一个小部件更改一个小部件,这里有一些想法:
1。使用 Platform.environment.containsKey('FLUTTER_TEST')
您可以从 dart:io
(not supported on web) or universal_io
导入 Platform
(网络支持)。
您的构建方法可以是:
@override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2。使用注解@visibleForTesting
您可以在测试文件中注释仅 visible/usable 的参数(例如:mockChild
):
class MyWidget extends StatelessWidget {
const MyWidget({
@visibleForTesting this.mockChild,
});
final Widget? child;
@override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
在你的测试中:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);
我正在尝试为我的 Flutter 应用程序创建测试。简单示例:
class MyWidget extends StatelessWidget {
@override
build(BuildContext context) {
return MySecondWidget();
}
}
我想验证 MyWidget
实际上是在调用 MySecondWidget
而不构建 MySecondWidget
。
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
在我的例子中,这将不起作用,因为 MySecondWidget
需要一些特定且复杂的设置(例如 API 键,Provider
中的值...)。我想要的是 "mock" MySecondWidget
是一个空的 Container
(例如),这样它在测试期间不会引发任何错误。
我该怎么做?
您可以模拟 MySecondWidget
(例如使用 Mockito),但您确实需要更改您的真实代码以在测试模式下创建 MockMySecondWidget
,因此它并不漂亮。 Flutter 不支持基于 Type
的对象实例化(通过 dart:mirrors
除外,但这与 Flutter 不兼容),因此您不能 'inject' 该类型作为依赖项。要确定您是否处于测试模式,请使用 Platform.environment.containsKey('FLUTTER_TEST')
- 最好在启动时确定一次并将结果设置为全局 final
变量,这将使任何条件语句快速。
一种方法是将子小部件包装到一个函数中,并将该函数传递给父小部件的构造函数:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
@override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
然后你可以制作你的模拟小部件,通过 buildMySecondWidgetFn
测试。
没有开箱即用的东西来模拟小部件。我打算写一些关于如何在测试期间“模拟”/替换小部件的 examples/ideas(例如 SizedBox.shrink()
.
但首先,让我解释一下为什么我认为这不是一个好主意。
在 Flutter 中,您正在构建一个小部件树。一个特定的widget有一个parent,通常有一个或几个children.
Flutter 出于性能原因选择了单通道布局算法(参见this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
据此,我们需要了解 parent 需要它的 children 来构建以获得它们的大小,然后正确地渲染自己。如果您删除它的 children,它的行为可能会完全不同。
如果可能,最好模拟服务。例如,如果您的 child 发出 HTTP 请求,您可以 mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
如果 child 需要特定的提供者,您可以提供一个虚拟的提供者:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
如果您在测试期间仍想用另一个小部件更改一个小部件,这里有一些想法:
1。使用 Platform.environment.containsKey('FLUTTER_TEST')
您可以从 dart:io
(not supported on web) or universal_io
导入 Platform
(网络支持)。
您的构建方法可以是:
@override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2。使用注解@visibleForTesting
您可以在测试文件中注释仅 visible/usable 的参数(例如:mockChild
):
class MyWidget extends StatelessWidget {
const MyWidget({
@visibleForTesting this.mockChild,
});
final Widget? child;
@override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
在你的测试中:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);