Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing Everything with Flutter

Miguel Beltran
October 08, 2020
760

Testing Everything with Flutter

Miguel Beltran

October 08, 2020
Tweet

Transcript

  1. Freelance Developer, specialized in Mobile (Android and Flutter) More than

    15 years of professional developer experience Co-Host of Code Cafeteria Podcast Reach me out: beltran.work About Me @MiBLT
  2. Testing: Dart, Widgets, Driver Mocks and Fakes in Dart Architecture

    + testing: DI and State Mgmt Limits of Flutter testing In this talk... @MiBLT
  3. Sample app: GIF REVIEW Purpose: Let users rate GIFs Screens:

    Main and detail Code: miquelbeltran/flutter_testing_talk The test subject @MiBLT
  4. Why do we need to test at all? “The more

    features your app has, the harder it is to test manually. Automated tests help ensure that your app performs correctly before you publish it, while retaining your feature and bug fix velocity.” - Flutter docs. @MiBLT
  5. Pure Dart tests that don’t load Widgets Dart Test Tests

    that load Widgets Widget Test Tests that run on a real device Driver @MiBLT
  6. Simple Dart test void main() { final ratings = [

    Rating('Miguel', 5), Rating('Lara', 4), Rating('Lily', 3), ]; test('calculate ratings', () { final average = calculateAverage(ratings); expect(average, 4); }); } @MiBLT
  7. Simple Dart test void main() { final ratings = [

    Rating('Miguel', 5), Rating('Lara', 4), Rating('Lily', 3), ]; test('calculate ratings', () { final average = calculateAverage(ratings); expect(average, 4); }); } @MiBLT
  8. Simple Dart test void main() { final ratings = [

    Rating('Miguel', 5), Rating('Lara', 4), Rating('Lily', 3), ]; test('calculate ratings', () { final average = calculateAverage(ratings); expect(average, 4); }); } @MiBLT
  9. GifScore Widget class GifScore extends StatelessWidget { Gif gif; GifScore(this.gif);

    @override Widget build(BuildContext context) { return ...; } } @MiBLT
  10. pumpWidget final gif = Gif( id: '1', title: 'Kermit Sipping

    Tea', url: 'https://...', rating: 3.5, ); await tester.pumpWidget( GifScore( gif, ), ); @MiBLT
  11. pumpWidget final gif = Gif( id: '1', title: 'Kermit Sipping

    Tea' , url: 'https://...', rating: 3.5, ); await tester.pumpWidget( GifScore( gif, ), ); @MiBLT
  12. Writing testWidgets testWidgets('display GifScore', (WidgetTester tester) async { mockNetworkImagesFor(() async

    { final gif = Gif( id: '1', title: 'Kermit Sipping Tea', url:'...', rating: 3.5, ); await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: GifScore( gif, ), ), ); expect(find.text('Kermit Sipping Tea'), findsOneWidget); }); }); @MiBLT
  13. Driver Tests Run on a real device (also simulators) Test

    full running app: Compile + Run + Test Let’s check the title is displayed Flutter 1.22: Android and iOS -> slower Beta/Master: Web, Desktop, etc -> faster tests! @MiBLT
  14. Driver Tests: app.dart import 'package:../main.dart' as app; void main() {

    enableFlutterDriverExtension(); app.main(); } @MiBLT
  15. Driver Tests: app_test.dart FlutterDriver driver; setUpAll(() async { driver =

    await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) { driver.close(); } }); test('title is displayed', () async { expect(await driver.getText(titleFinder), "Kermit Sipping Tea"); }); @MiBLT
  16. Driver Tests: app_test.dart final gifFinder = find.byValueKey('gif-score'); final titleFinder =

    find.descendant( of: gifFinder, matching: find.byValueKey('gif-title'), ); @MiBLT
  17. Complex to Setup Require a device Hard to debug Real

    user experience Driver Tests @MiBLT
  18. SLOW UNRELIABLE MOST COMPLEX REAL USER Driver FAST RELIABLE EASY

    NOT REAL USER Dart FAST RELIABLE SOMEHOW COMPLEX SOMEHOW REAL USER Widget Testing Options Summary SPEED: STABLE: CODE: USER: @MiBLT
  19. HomePage Widget class HomePage extends StatelessWidget { @override Widget build(BuildContext

    context) { final api = ApiService(); return Scaffold( appBar: AppBar( title: Text('GifReview'), ), body: FutureBuilder<List<Gif>>( future: api.getGifs(), builder: (context, snap) { ... }, ), ); } } @MiBLT
  20. Dependencies in tests Code that depends on other components is

    hard to test. Provide those components externally → Dependency Injection. DI in Flutter: - via constructor - Provider (through Widget tree) - Get_it (as Service Locator) @MiBLT
  21. DI via constructor void main() { final service = FakeApiService();

    final usecase = UseCase(service); test('test my UseCase', () { final result = usecase.run(); expect(result, expected); }); } @MiBLT
  22. DI via Provider class MyApp extends StatelessWidget { @override Widget

    build(BuildContext context) { return Provider<ApiService>( create: (context) => RealApiService(), child: MaterialApp(...), ); } } // later in code: service = Provider.of<ApiService>(context) @MiBLT
  23. DI via Provider await tester.pumpWidget ( Provider<ApiService>( create: (context) =>

    FakeApiService() , child: MyWidget(...), ), ); @MiBLT
  24. Fakes and Mocks Custom implementation of a class that fakes

    its functionality Fake Object that allows to configure answers, capture parameters and verify those calls Mock @MiBLT
  25. FakeApiService class _FakeApiService extends ApiService { final gif = Gif(

    id: '1', title: 'Kermit Sipping Tea' , Url: '...', rating: 3.5, ); @override Future<List<Gif>> getGifs() { return SynchronousFuture ([gif]); } } @MiBLT
  26. FakeApiService testWidgets('load homescreen with fakes', (WidgetTester tester) async { mockNetworkImagesFor(()

    async { final fakeApiService = _FakeApiService(); await tester.pumpWidget( Provider<ApiService>( create: (context) => fakeApiService, child: MaterialApp(home: HomePage()), ), ); expect(find.text('Kermit Sipping Tea'), findsOneWidget); }); }); @MiBLT
  27. MockApiService https://pub.dev/packages/mockito 1 → class MockApiService extends Mock implements ApiService

    {} 2 → final mockApiService = MockApiService(); 3 → when(mockApiService.getGifs()) .thenAnswer((realInvocation) => SynchronousFuture([gif])); @MiBLT
  28. MockApiService testWidgets('load homescreen with mocks', (tester) async { mockNetworkImagesFor(() async

    { //... await tester.pumpWidget ( Provider<ApiService>( create: (context) => mockApiService , child: MaterialApp (home: HomePage()), ), ); expect(find.text('Kermit Sipping Tea' ), findsOneWidget ); verify(mockApiService .getGifs()); }); }); @MiBLT
  29. Fakes and Mocks NO PACKAGE EASIER TO WRITE NEEDS EXTRA

    WORK TO CAPTURE AND VERIFY CALLS Fake NEEDS PACKAGE HARDER TO WRITE CAN CAPTURE AND VERIFY CALLS BY DEFAULT Mock @MiBLT
  30. Test Pyramid https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html Unit test “take a small piece of

    the product and test that piece in isolation.” Unit Tests Integration Tests E2E @MiBLT
  31. Test Pyramid https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html Integration Tests “takes a small group of

    units, and tests their behavior as a whole, verifying that they coherently work together.” Unit Tests Integration Tests E2E @MiBLT
  32. Test Pyramid and Flutter Unit Tests Integration Tests E2E Flutter

    Driver Complex Dart & Widget Tests Simple Dart & Widget Tests @MiBLT
  33. % of app code executed when tests run. Supported by

    Dart and Widget tests. Run with flutter test --coverage Running tests with coverage @MiBLT @MiBLT
  34. Coverage reports $ lcov --summary lcov.info Reading tracefile lcov.info Summary

    coverage rate: lines......: 25.4% (18 of 71 lines) functions..: no data found branches...: no data found $ lcov -l lcov.info Also compatible with codecov.io! @MiBLT
  35. mockNetworkImagesFor Flutter testing framework creates a fake HTTP Client Always

    returns HTTP Error 400 Why? Networking in tests is bad How can we have images? Override the default mock HTTP Client Provide empty images on network calls Package: network_image_mock @MiBLT
  36. tester.pump(duration) Testing and animations WidgetTester is not rebuilding and advancing

    the clock How to wait them to complete? Call tester.pump(duration) Call tester.pumpAndSettle @MiBLT
  37. Network calls in Dart test (and skipping tests) test('network call',

    () async { String url = 'http://example.com/api/items/1' ; Http.Response response = await Http.get(url); expect(response.statusCode, 200); }, skip: true); @MiBLT
  38. SynchronousFuture Fake Future that responds immediately Part of the Flutter

    framework Use it in Widget tests with mocked network calls when(mockApiService .getGifs()) .thenAnswer((realInvocation ) => SynchronousFuture ([gif])); @MiBLT
  39. More testing approaches integration_test package Allows you to run Widget

    tests on devices/emulators Same API as WidgetTester Used in some Flutter plugins Can run on Firebase Test Lab espresso package Test Flutter code from Espresso Android tests @MiBLT
  40. RECAP 3 ways of testing Dart tests: Fast and reliable,

    no Widget testing Widget tests: Also fast and can test Widgets Flutter Driver: Run on a real device Architecture is important! → Dependency Injection Mocks and Fakes, use them! @MiBLT