简体   繁体   English

如何为使用连接插件的小部件编写 Flutter 小部件测试

[英]How to write Flutter widget test for a widget that uses the Connectivity plugin

I have a single screen that uses the connectivity package from the Flutter dev team.我有一个使用 Flutter 开发团队的连接package 的屏幕。 I am able to build golden images for this widget when I don't use the connectivity package, but when I add it the way that is outlined by the Flutter dev team on their page on pub.dev, I encounter exceptions when running the tests via the flutter test --update-goldens command.当我不使用连接 package 时,我能够为此小部件构建黄金图像,但是当我按照 Flutter 开发团队在其 pub.dev 页面上概述的方式添加它时,我在运行测试时遇到异常通过flutter test --update-goldens命令。

I have included the test file ( test/widget/widget_test.dart ), main.dart , welcome_screen.dart , and the output from running the tests. I have included the test file ( test/widget/widget_test.dart ), main.dart , welcome_screen.dart , and the output from running the tests. I tried looking for similar issues that others have experienced online, but my efforts were not fruitful;我尝试寻找其他人在网上遇到过的类似问题,但我的努力没有结果; I am looking for help in resolving this issue.我正在寻求解决此问题的帮助。 Any advice or suggestions would be greatly appreciated!任何意见或建议将不胜感激!

Output Output

from flutter test --update-goldens来自flutter test --update-goldens

══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
The following MissingPluginException was thrown while activating platform stream on channel
plugins.flutter.io/connectivity_status:
MissingPluginException(No implementation found for method listen on channel
plugins.flutter.io/connectivity_status)

When the exception was thrown, this was the stack:
#0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following MissingPluginException was thrown running a test:
MissingPluginException(No implementation found for method check on channel
plugins.flutter.io/connectivity)

When the exception was thrown, this was the stack:
#0      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:156:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...

The test description was:
  WelcomeScreen Golden test
════════════════════════════════════════════════════════════════════════════════════════════════════
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following message was thrown:
Multiple exceptions (2) were detected during the running of the current test, and at least one was
unexpected.
════════════════════════════════════════════════════════════════════════════════════════════════════
00:04 +14 -1: /Users/---/Documents/---/---/---/test/widget/widget_test.dart: WelcomeScreen Golden test [E]                                                                                                                      
  Test failed. See exception logs above.
  The test description was: WelcomeScreen Golden test
  
00:04 +14 -1: Some tests failed.

Test: widget_test.dart测试:widget_test.dart

void main() {
  testWidgets('WelcomeScreen Golden test', (WidgetTester tester) async {
    await tester.pumpWidget(MyApp());
    await expectLater(
      find.byType(MyApp),
      matchesGoldenFile('main.png'),
    );
  });
}

main.dart main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        FocusManager.instance.primaryFocus.unfocus();
      },
      child: MaterialApp(
        initialRoute: WelcomeScreen.id,
        routes: {
          WelcomeScreen.id: (context) => WelcomeScreen(),
          DashboardScreen.id: (context) => DashboardScreen(),
        },
      ),
    );
  }
}

Screen: welcome_screen.dart屏幕:welcome_screen.dart

class WelcomeScreen extends StatefulWidget {
  static const String id = 'welcome_screen';

  @override
  _WelcomeScreenState createState() => _WelcomeScreenState();
}

class _WelcomeScreenState extends State<WelcomeScreen> {
  
  ConnectivityResult _connectionStatus = ConnectivityResult.none;
  final Connectivity _connectivity = Connectivity();
  StreamSubscription<ConnectivityResult> _connectivitySubscription;

  String username = '';
  String password = '';
  bool isLoading = false;

  @override
  void initState() {
    super.initState();

    _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
    initConnectivity();
  }

  @override
  void dispose() {
    _connectivitySubscription.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: Padding(
        padding: EdgeInsets.symmetric(horizontal: 24.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            Image.asset('images/logo.png'),
            RoundedTextField(
              textInputAction: TextInputAction.next,
              placeholder: 'Username',
              icon: Icons.person,
              color: Colors.lightBlueAccent,
              onChanged: (value) {
                setState(() {
                  username = value;
                });
              },
            ),
            RoundedTextField(
              textInputAction: TextInputAction.done,
              placeholder: 'Password',
              icon: Icons.lock,
              color: Colors.lightBlueAccent,
              password: true,
              onChanged: (value) {
                setState(() {
                  password = value;
                });
              },
            ),
            isLoading
                ? Center(
                    child: Padding(
                      padding: EdgeInsets.symmetric(vertical: 22.0),
                      child: CircularProgressIndicator(),
                    ),
                  )
                : RoundedButton(
                    disabled: isLoading,
                    title: 'Log In',
                    color: Colors.lightBlueAccent,
                    onPressed: (_connectionStatus == ConnectivityResult.mobile || _connectionStatus == ConnectivityResult.wifi)
                        ? () async {
                            setState(() {
                              isLoading = true;
                            });
                            try {
                              Login login = await API().login(username, password);
                              if (login.appUserKey != 0) {
                                Navigator.pushNamed(context, DashboardScreen.id);
                              }
                            } catch (e) {
                              print(e);
                            }
                            setState(() {
                              isLoading = false;
                            });
                          }
                        : null,
                  ),
          ],
        ),
      ),
    );
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initConnectivity() async {
    ConnectivityResult result;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      result = await _connectivity.checkConnectivity();
    } on PlatformException catch (e) {
      print(e.toString());
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) {
      return Future.value(null);
    }

    return _updateConnectionStatus(result);
  }

  Future<void> _updateConnectionStatus(ConnectivityResult result) async {
    switch (result) {
      case ConnectivityResult.wifi:
      case ConnectivityResult.mobile:
        setState(() => _connectionStatus = result);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Connected to network'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
      case ConnectivityResult.none:
        setState(() => _connectionStatus = result);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Disconnected from network'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
      default:
        setState(() => _connectionStatus = null);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Connectivity failed'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
    }
  }
}

I resolved my issue.我解决了我的问题。

The widget was trying to run a method that did not exist for the platform on which the tests were being run.小部件正在尝试运行在运行测试的平台上不存在的方法。 I decided to mock the Connectivity class, send the mocked class to the widget, and then have the widget itself check to see what type of class it had received in order to determine if it should attempt to run the listen method;我决定模拟连接 class,将模拟的 class 发送到小部件,然后让小部件本身检查什么类型的 class 以确定它是否应该尝试运行它收到的监听方法; the method whose invocation had previously been causing exceptions to be thrown.该方法的调用先前已导致引发异常。 I used the Mockito package to easily mock the service.我使用Mockito package 轻松模拟该服务。

I've included the relevant code snippets that resolved the issue for me.我已经包含了为我解决问题的相关代码片段。

Test: widget_test.dart测试:widget_test.dart

Important: Used Mockito to mock the Connectivity class重要提示:使用Mockito模拟连接 class

class MockConnectivity extends Mock implements Connectivity {}

void main() {
  testWidgets('WelcomeScreen', (WidgetTester tester) async {
    await tester.runAsync(() async {
      await tester.pumpWidget(
        MaterialApp(
          home: WelcomeScreen(
            connectivity: MockConnectivity(),
          ),
        ),
      );
      await tester.pumpAndSettle();
    });

    await expectLater(
      find.byType(WelcomeScreen),
      matchesGoldenFile('welcome_screen_portrait.png'),
    );
  });
}

main.dart main.dart

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: WelcomeScreen.id,
      routes: {
        WelcomeScreen.id: (context) => WelcomeScreen(
          connectivity: Connectivity(),
        ),
      },
    );
  }
}

welcome_screen.dart Welcome_screen.dart

class WelcomeScreen extends StatefulWidget {
  static const String id = 'welcome_screen';
  final Connectivity connectivity;
  WelcomeScreen({this.connectivity});

  @override
  _WelcomeScreenState createState() => _WelcomeScreenState();
}

class _WelcomeScreenState extends State<WelcomeScreen> {
  Connectivity _connectivity;
  ConnectivityResult _connectionStatus = ConnectivityResult.none;
  StreamSubscription<ConnectivityResult> _connectivitySubscription;

  @override
  void initState() {
    super.initState();

    _connectivity = widget.connectivity;
    if (_connectivity.runtimeType == Connectivity) {
      _connectivitySubscription = _connectivity.onConnectivityChanged.listen(_updateConnectionStatus);
    }
    initConnectivity();
  }

  @override
  void dispose() {
    controller.dispose();
    if (_connectivity.runtimeType == Connectivity) {
      _connectivitySubscription.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text('Welcome'),
    );
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initConnectivity() async {
    ConnectivityResult result;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      result = await _connectivity.checkConnectivity();
    } on PlatformException catch (e) {
      print(e.toString());
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) {
      return Future.value(null);
    }

    return _updateConnectionStatus(result);
  }

  Future<void> _updateConnectionStatus(ConnectivityResult result) async {
    switch (result) {
      case ConnectivityResult.wifi:
      case ConnectivityResult.mobile:
        setState(() => _connectionStatus = result);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Connected to network'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
      case ConnectivityResult.none:
        setState(() => _connectionStatus = result);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Disconnected from network'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
      default:
        setState(() => _connectionStatus = null);
        ScaffoldMessenger.of(context).hideCurrentSnackBar();
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('Connectivity failed'),
            duration: Duration(seconds: 1),
          ),
        );
        break;
    }
  }
}

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

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