简体   繁体   中英

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. 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.

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

from 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

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

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

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; the method whose invocation had previously been causing exceptions to be thrown. I used the Mockito package to easily mock the service.

I've included the relevant code snippets that resolved the issue for me.

Test: widget_test.dart

Important: Used Mockito to mock the Connectivity 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

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

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;
    }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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