简体   繁体   English

在 Flutter 测试中模拟一个 Widget

[英]Mock a Widget in Flutter tests

I am trying to create tests for my Flutter application.我正在尝试为我的 Flutter 应用程序创建测试。 Simple example:简单示例:

class MyWidget extends StatelessWidget {
   @override
   build(BuildContext context) {
      return MySecondWidget();
   }
}

I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget .我想验证MyWidget实际上是在调用MySecondWidget而不构建MySecondWidget

void main() {
   testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
      await tester.pumpWidget(MyWidget());
      expect(find.byType(MySecondWidget), findsOneWidget);
   }
}

In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider ...).在我的例子中,这将不起作用,因为MySecondWidget需要一些特定和复杂的设置(比如 API 键, Provider中的值......)。 What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.我想要的是将MySecondWidget “模拟”为一个空Container (例如),这样它就不会在测试期间引发任何错误。

How can I do something like that?我怎么能做那样的事情?

There is nothing done out of the box to mock a widget.没有开箱即用的东西来模拟小部件。 I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink() .我将写一些关于如何在测试期间“模拟”/替换小部件的示例/想法(例如使用SizedBox.shrink()

But first, let me explain why I think this is not a good idea.但首先,让我解释一下为什么我认为这不是一个好主意。

In Flutter you are building a widget tree.在 Flutter 中,您正在构建一个小部件树。 A specific widget has a parent and usually has one or several children.一个特定的部件有一个父部件,通常有一个或多个子部件。

Flutter chose a single pass layout algorithm for performance reasons (see this ): Flutter 出于性能原因选择了单通道布局算法(请参阅):

Flutter performs one layout per frame, and the layout algorithm works in a single pass. Flutter 每帧执行一次布局,布局算法在单次通过中工作。 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.重要的是,一旦渲染 object 从其布局方法返回,渲染 object 将不会再次访问,直到下一帧的布局。 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.这种方法将本来可能是单独的测量和布局传递组合成一个传递,因此,每个渲染 object 在布局期间最多被访问两次:一次在树下的路上,一次在树上的路上。

From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly.由此,我们需要了解父母需要它的孩子建造以获得他们的尺寸,然后正确地渲染自己。 If you remove its children, it might behave completely differently.如果您删除它的子项,它的行为可能会完全不同。

It is better to mock the services if possible.如果可能,最好模拟服务。 For example, if your child makes an HTTP request, you can mock the HTTP client :例如,如果您的孩子发出 HTTP 请求,您可以模拟 HTTP 客户端

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

If the child needs a specific provider you can provide a dummy one:如果孩子需要特定的提供者,您可以提供一个虚拟的提供者:

testWidgets('My test', (tester) async {
  tester.pumpWidget(
    Provider<MyProvider>(
      create: (_) => MyDummyProvider(),
      child: MyWidget(),
    ),
  );
});

If you still want to change a widget with another one during your tests, here are some ideas:如果您仍然想在测试期间用另一个小部件更改一个小部件,这里有一些想法:

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

You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).您可以从dart:io (网络不支持)或universal_io (网络支持)导入Platform

and your build method could be:你的构建方法可能是:

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

2. Use the annotation @visibleForTesting 2.使用注解@visibleForTesting

You can annotate a parameter (ex: mockChild ) that is only visible/usable in a test file:您可以注释仅在测试文件中可见/可用的参数(例如: 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.
  }
}

And in your test:在你的测试中:

tester.pumpWidget(
  MyWidget(
    mockChild: MyMockChild(),
  ),
);

You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty.您可以模拟MySecondWidget (例如使用 Mockito),但您确实需要更改您的真实代码以在测试模式下创建MockMySecondWidget ,因此它并不漂亮。 Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. Flutter 不支持基于Type对象实例化(除了通过dart:mirrors但与 Flutter 不兼容),因此您不能将类型作为依赖项“注入”。 To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.要确定您是否处于测试模式,请使用Platform.environment.containsKey('FLUTTER_TEST') - 最好在启动时确定一次并将结果设置为全局final变量,这将使任何条件语句快速。

One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:一种方法是将子小部件包装到 function 中,并将 function 传递给父小部件的构造函数:

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();

Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.然后你可以构建你的模拟小部件,在测试中通过buildMySecondWidgetFn传递它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM