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

Testing Everything with Flutter

72ffb135de71bef2c4a11961634edc6a?s=47 Miguel Beltran
October 08, 2020
320

Testing Everything with Flutter

72ffb135de71bef2c4a11961634edc6a?s=128

Miguel Beltran

October 08, 2020
Tweet

Transcript

  1. Testing everything with Flutter Miguel Beltran Freelancer - beltran.work -

    @MiBLT
  2. 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
  3. Testing: Dart, Widgets, Driver Mocks and Fakes in Dart Architecture

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

    Main and detail Code: miquelbeltran/flutter_testing_talk The test subject @MiBLT
  5. 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
  6. 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
  7. Writing a Dart test @MiBLT

  8. Simple Dart test double calculateAverage(List<Rating> ratings) { return // calculate

    average } @MiBLT
  9. @MiBLT

  10. Simple Dart test import 'package:flutter_test/flutter_test.dart'; void main() { test('calculate ratings',

    () { // body of test }); } @MiBLT
  11. 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
  12. 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
  13. 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
  14. Assert that “actual” matches “matcher”. expect(actual, matcher) @MiBLT

  15. Expect expect(average, 4); // same as expect(average, equals(4)); @MiBLT

  16. Expect expect(myArray, isNotEmpty); expect(myWidget, findsOneWidget); expect(myBool, isTrue); @MiBLT

  17. Also with right-click -> Run test in…. or $ flutter

    test @MiBLT
  18. Writing a Widget test @MiBLT

  19. Testing “GifScore” Text Image.network SmoothStarRating @MiBLT

  20. Testing “GifScore” Text @MiBLT

  21. GifScore Widget class GifScore extends StatelessWidget { Gif gif; GifScore(this.gif);

    @override Widget build(BuildContext context) { return ...; } } @MiBLT
  22. Writing testWidgets testWidgets('display GifScore', (WidgetTester tester) async { /// ...

    }); }); @MiBLT
  23. Writing testWidgets testWidgets('display GifScore', (WidgetTester tester) async { /// ...

    }); }); @MiBLT
  24. pumpWidget final gif = Gif( id: '1', title: 'Kermit Sipping

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

    Tea' , url: 'https://...', rating: 3.5, ); await tester.pumpWidget( GifScore( gif, ), ); @MiBLT
  26. pumpWidget await tester.pumpWidget( Directionality( textDirection: TextDirection.rtl, child: GifScore( gif, ),

    ), ); @MiBLT
  27. Writing testWidgets expect( find.text('Kermit Sipping Tea'), findsOneWidget ); @MiBLT

  28. Writing testWidgets find.byKey(key) find.bySemanticsLabel(label) find.descendant( of: find.byKey(key), matching: find.text(text), )

    @MiBLT
  29. 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
  30. Writing a Driver test @MiBLT

  31. 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
  32. @MiBLT

  33. Driver Tests: app.dart import 'package:../main.dart' as app; void main() {

    enableFlutterDriverExtension(); app.main(); } @MiBLT
  34. 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
  35. Driver Tests: app_test.dart final gifFinder = find.byValueKey('gif-score'); final titleFinder =

    find.descendant( of: gifFinder, matching: find.byValueKey('gif-title'), ); @MiBLT
  36. Driver Tests: Running @MiBLT

  37. Driver Tests: failed? @MiBLT

  38. Driver Tests: Failed? @MiBLT

  39. Driver Tests: Success @MiBLT

  40. Complex to Setup Require a device Hard to debug Real

    user experience Driver Tests @MiBLT
  41. 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
  42. Dependency Injection, Fakes, Mocks, and making your app “testable” @MiBLT

  43. Tests so far final average = calculateAverage(ratings); await tester.pumpWidget(GifScore(gif)); No

    internal dependencies! @MiBLT
  44. 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
  45. Pass our ApiService Fake the ApiService Challenges @MiBLT

  46. Pass our ApiService: Dep. Inj. Fake the ApiService: Fake or

    Mock Challenges @MiBLT
  47. 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
  48. DI via constructor class UseCase { final ApiService service; UseCase(this.service);

    Gif run() { ... } } @MiBLT
  49. DI via constructor void main() { final service = FakeApiService();

    final usecase = UseCase(service); test('test my UseCase', () { final result = usecase.run(); expect(result, expected); }); } @MiBLT
  50. 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
  51. DI via Provider await tester.pumpWidget ( Provider<ApiService>( create: (context) =>

    FakeApiService() , child: MyWidget(...), ), ); @MiBLT
  52. Which State Management do you recommend? “Whatever allows you to

    test your code!” - Me @MiBLT
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. Coverage and having “enough” tests @MiBLT

  60. Test Pyramid Unit Tests Integration Tests E2E https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html @MiBLT

  61. 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
  62. 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
  63. Test Pyramid https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html End to End “Build and run a

    real app, run automated tests on it” Unit Tests Integration Tests E2E @MiBLT
  64. Test Pyramid Unit Tests Integration Tests E2E https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html 10% E2E

    20% Integration Tests 70% Unit Tests @MiBLT
  65. Test Pyramid and Flutter Unit Tests Integration Tests E2E Flutter

    Driver Widget Tests Dart Tests @MiBLT
  66. Test Pyramid and Flutter Unit Tests Integration Tests E2E Flutter

    Driver Widget Tests Dart Tests @MiBLT
  67. Test Pyramid and Flutter Unit Tests Integration Tests E2E Flutter

    Driver Complex Dart & Widget Tests Simple Dart & Widget Tests @MiBLT
  68. https://flutter.dev/docs/testing @MiBLT

  69. “See Widget tests as Unit Tests” @MiBLT @MiBLT

  70. % of app code executed when tests run. Supported by

    Dart and Widget tests. Run with flutter test --coverage Running tests with coverage @MiBLT @MiBLT
  71. Coverage reports @MiBLT

  72. 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
  73. Coverage reports @MiBLT

  74. Recommended test coverage in Flutter by me 70% @MiBLT

  75. Edge cases you should know @MiBLT

  76. mockNetworkImagesFor testWidgets('..', (tester) async { mockNetworkImagesFor (() async { });

    }); @MiBLT
  77. mockNetworkImagesFor Text Image.network SmoothStarRating @MiBLT

  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. Miguel Beltran Freelancer Twitter: @MiBLT Reach me out: beltran.work Repo:

    miquelbeltran/flutter_testing_talk THANK YOU!