在Flutter测试中模拟小部件

10

我正在尝试为我的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 中的值等)。我想要的是“模拟”MySecondWidget成为空的Container(例如),以便在测试过程中不会引发任何错误。
我该怎么做呢?

你最终解决了这个问题吗? - jgv115
恐怕这可能不容易实现。 - Valentin Vignal
3个回答

6

目前,Flutter没有为模拟小部件提供开箱即用的解决方案。我将提供一些示例/想法,说明如何在测试期间“模拟”/替换小部件(例如使用SizedBox.shrink())。

但首先,让我解释一下为什么我认为这不是一个好主意。

在Flutter中,您正在构建一个小部件树形结构。特定的小部件有一个父级,通常有一个或几个子级。

Flutter出于性能原因选择了单次布局算法(请参见此文档):

Flutter每帧执行一次布局,布局算法在单次传递中运行。约束条件从父对象通过在每个子代上调用布局方法来传递到下面。子代递归执行自己的布局,然后通过从其布局方法返回来向上发送几何体。重要的是,一旦渲染对象从其布局方法返回,该渲染对象在下一帧的布局之前不会再次被访问。此方法将可能分开的测量和布局传递组合成单个传递,并且结果是,在布局期间,每个渲染对象最多只访问两次:一次在向下遍历树时,一次在向上遍历树时。

由此可知,父级需要其子级构建以获取它们的大小并正确呈现自己。如果您删除了其子级,它可能会表现完全不同。

最好模拟服务(如果可能)。例如,如果您的子级发出HTTP请求,则可以模拟HTTP客户端

HttpOverrides.runZoned(() {
  // Operations will use MyHttpClient instead of the real HttpClient
  // implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));

如果孩子需要特定的提供者,您可以提供虚拟的提供者:
testWidgets('My test', (tester) async {
  tester.pumpWidget(
    Provider<MyProvider>(
      create: (_) => MyDummyProvider(),
      child: MyWidget(),
    ),
  );
});

如果你想在测试期间使用其他的控件替换一个小部件,下面提供一些思路:

1. 使用 Platform.environment.containsKey('FLUTTER_TEST')

你可以从dart:io导入Platform(不支持Web),或者从universal_io导入(支持Web)。

你的建造方法可能是这样的:

@override
Widget build(BuildContext context) {
  final isTest = Platform.environment.containsKey('FLUTTER_TEST');
  if (isTest) return const SizedBox.shrink();
  return // Your real implementation.
}

2. 使用注解@visibleForTesting

您可以对仅在测试文件中可见/可用的参数(例如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(),
  ),
);

谢谢您,我花了几个小时来尝试解决这个问题,而第一种解决方案起了作用。 - Fi Li Ppo

0
一种方法是将子小部件包装到一个函数中,并将该函数传递给父小部件的构造函数:
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传递它。


这样做会违反Flutter指南,该指南表示通过方法构建小部件不是一个好的实践。https://dev59.com/plQJ5IYBdhLWcg3wuoX7#53234826 https://www.youtube.com/watch?v=IOyq-eTRhvo - Valentin Vignal
如果你仔细观看视频,会发现构建单独的小部件比使用辅助方法更好,原因如下:
  • 性能
  • 可测试性
  • 准确性
请考虑这些原因是否在你的情况下仍然适用(特别是关于可测试性),而不是盲目地遵循指南。
- Dennis Cheng

-1

您可以模拟MySecondWidget(例如使用Mockito),但是在测试模式下,您确实需要更改真实代码以创建MockMySecondWidget,因此这不太美观。Flutter不支持基于Type的对象实例化(除非通过dart:mirrors,但这与Flutter不兼容),因此您无法将类型“注入”为依赖项。要确定是否处于测试模式,请使用Platform.environment.containsKey('FLUTTER_TEST') - 最好在启动时仅确定一次并将结果设置为全局final变量,这将使任何条件语句快速执行。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接