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

flutter_kaigi_2021.pdf

Kyohei Ito
November 30, 2021

 flutter_kaigi_2021.pdf

Kyohei Ito

November 30, 2021
Tweet

More Decks by Kyohei Ito

Other Decks in Programming

Transcript

  1. Golden Testing void main() { testWidgets('Snapshot for MyApp', (tester) async

    { const app = MyApp(); // ΩϟϓνϟαΠζΛઃఆ await tester.binding.setSurfaceSize(const Size(414, 896)); await tester.pumpWidget(app); // Ϛελʔεφοϓγϣοτͱͷࠩ෼ൺֱ await expectLater( find.byWidget(app), matchesGoldenFile('MyApp.png'), ); }); }
  2. Golden Testing mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text( 'You have

    pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ※ϓϩδΣΫτ࡞੒࣌ʹ࡞ΒΕΔίʔυ͔Βൈਮ
  3. Golden Testing mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ const Text( 'You have

    pushed the button this many times:', ), Text( '$_counter', style: Theme.of(context).textTheme.headline4, ), ], ※ϓϩδΣΫτ࡞੒࣌ʹ࡞ΒΕΔίʔυ͔Βൈਮ DFOUFSΛTUBSUʹมߋͯ͠ςετͯ͠ΈΔ
  4. Golden Testing $ flutter test 00:04 +0: Snapshot for MyApp

    00:04 +0 -1: Snapshot for MyApp [E] Test failed. See exception logs above. The test description was: Snapshot for MyApp The test description was: Snapshot for MyApp 00:04 +0 -1: Some tests failed.
  5. reg-suit $ yarn add reg-suit $ yarn reg-suit init --use-yarn

    ? Plugin(s) to install (bold: recommended) (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Working directory of reg-suit. .reg ? Append ".reg" entry to your .gitignore file. Yes ? Directory contains actual images. catalog_app/test/screenshots ? Threshold, ranges from 0 to 1. Smaller value makes the comparison more sensitive. 0 ? notify-github plugin requires a client ID of reg-suit GitHub app. Open installation window in your browser Yes ? This repositoriy's client ID of reg-suit GitHub app ? Create a new GCS bucket No ? Existing bucket name ? Update configuration file Yes ? Copy sample images to working dir No
  6. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); }
  7. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); } Playbook ΠϯελϯεΛड͚औΓΧλϩάΞϓϦશମΛߏங͢Δ
  8. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); } ड͚औͬͨ Story ෼ͷϦετ͕ PlaybookGallery ʹදࣔ͞ΕΔ
  9. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); } ड͚औͬͨ Scenario ෼ͷϦετ͕ Story ͷ Row ʹදࣔ͞ΕΔ
  10. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); } ड͚औͬͨ Widget ͷදࣔʹؔ͢Δ৘ใΛઃఆͰ͖Δ
  11. playbook-ui (playbook- fl utter) @override Widget build(BuildContext context) { return

    MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]), ]), ), ); } ಠࣗʹ࣮૷ͨ͠ Widget ͳͲΛ౉͢͜ͱͰදࣔ͞ΕΔ
  12. playbook-ui (playbook- fl utter) name: simple_catalog_app dependencies: flutter: sdk: flutter

    simple_app: path: ../simple_app playbook_ui: playbook: UI ΧλϩάΞϓϦߏ੒ྫ ϓϩμΫτͷΞϓϦΛґଘʹ௥Ճ
  13. playbook-ui (playbook- fl utter) name: component dependencies: flutter: sdk: flutter

    UI ίϯϙʔωϯτύοέʔδԽྫ දࣔʹඞཁͳॲཧ͚ͩͷύοέʔδΛ࡞੒
  14. playbook-ui (playbook- fl utter) name: simple_app dependencies: flutter: sdk: flutter

    component: path: '../component' σόΠεઐ༻ͷґଘͳͲ΋ΞϓϦଆʹهड़͠ඞཁʹԠͯ͡ίϯϙʔωϯτʹ DI ࡞੒ͨ͠6*ίϯϙʔωϯτΛ௥Ճ
  15. playbook-ui (playbook- fl utter) name: simple_catalog_app dependencies: flutter: sdk: flutter

    component: path: '../component' playbook_ui: playbook: ಉ͘͡6*ίϯϙʔωϯτΛ௥Ճ දࣔʹඞཁͳίϯϙʔωϯτ͚ͩΛ import ͢Δ͜ͱͰϏϧυ͕࣌ؒ୹ॖ
  16. playbook-ui (playbook- fl utter) void main() { runApp(MyApp()); } class

    MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child: ..., ), ]) ]), ), ); } } UI ΧλϩάΞϓϦ͕׬੒
  17. Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child:

    Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); playbook-snapshot (playbook- fl utter)
  18. Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child:

    Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); playbook-snapshot (playbook- fl utter) 1MBZCPPLʹSVOϝιου͕௥Ճ͞ΕΔ
  19. Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child:

    Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); playbook-snapshot (playbook- fl utter) 4DFOBSJP͕͍࣋ͬͯΔXJEHFU͕౉͞ΕΔ ΩϟϓνϟΛࡱΓ͍ͨ8JEHFU
  20. Playbook(stories: [ Story('Sample Widget', scenarios: [ Scenario( 'Sample Scenario', child:

    Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); playbook-snapshot (playbook- fl utter) Snapshot ઃఆྫ
  21. Future<void> main() async { await Playbook(stories: [ Story('Sample Widget', scenarios:

    [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); } playbook-snapshot (playbook- fl utter)
  22. Future<void> main() async { await Playbook(stories: [ Story('Sample Widget', scenarios:

    [ Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]).run( Snapshot( directoryPath: 'screenshots', devices: [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); } playbook-snapshot (playbook- fl utter) ͜ͷ෦෼͸ΞϓϦ΋ςετ΋ڞ௨
  23. Playbook playbook() { return Playbook(stories: [ Story('Sample Widget', scenarios: [

    Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]); } playbook-snapshot (playbook- fl utter)
  24. void main() { runApp(MyApp()); } class MyApp extends StatelessWidget {

    @override Widget build(BuildContext context) { return MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: playbook(), ), ); } } playbook-snapshot (playbook- fl utter)
  25. Future<void> main() async { await playbook().run( Snapshot( directoryPath: 'screenshots', devices:

    [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); } playbook-snapshot (playbook- fl utter)
  26. • தͰ͸ fl utter_test ͷ matchesGoldenFile Λ࣮ߦ͍ͯ͠Δ • ϑΥϯτ΍ΞΠίϯ͕ಡΈࠐΊͳ͍໰୊ͷαϙʔτ playbook-snapshot

    (playbook- fl utter) ৄ͘͠͸ 
 https://speakerdeck.com/tomokitakahashi/shi-jian- fl utter-visual-regression-testing
  27. name: simple_catalog_app flutter: fonts: - family: Roboto fonts: - asset:

    fonts/Roboto-Regular.ttf playbook-snapshot (playbook- fl utter) Ωϟϓνϟ࣌ͷϑΥϯτΛ௥Ճ͢Δ
  28. Future<void> main() async { await playbook().run( Snapshot( directoryPath: 'screenshots', devices:

    [SnapshotDevice.iPhone8], ), (widget) { return ProviderScope( overrides: [], MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ), ); }, ); } DI ͜ͷ͋ͨΓͰ%*Մೳ
  29. Playbook playbook() { return Playbook(stories: [ Story('Sample Widget', scenarios: [

    Scenario( 'Sample Scenario', child: Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ), ), ]) ]); } playbook-generator (playbook- fl utter) Before
  30. const storyTitle = 'Sample Widget'; @GenerateScenario(title: 'Sample Scenario') Widget sampleScenario()

    => Center( child: Container( width: 200, height: 250, color: Colors.amber, alignment: Alignment.center, child: const Text('Hello World'), ), ); playbook-generator (playbook- fl utter) After
  31. void main() { runApp(MyApp()); } class MyApp extends StatelessWidget {

    @override Widget build(BuildContext context) { return MaterialApp( title: 'Playbook Demo', theme: ThemeData.light(), home: PlaybookGallery( title: 'Sample App', playbook: playbook, ), ); } } playbook-generator (playbook- fl utter) ม਺ HFUUFS ͕ࣗಈੜ੒͞Ε͍ͯΔ
  32. Future<void> main() async { await playbook.run( Snapshot( directoryPath: 'screenshots', devices:

    [SnapshotDevice.iPhone8], ), (widget) { return MaterialApp( debugShowCheckedModeBanner: false, home: Material(child: widget), ); }, ); } playbook-generator (playbook- fl utter) ม਺ HFUUFS ͕ࣗಈੜ੒͞Ε͍ͯΔ
  33. { "dependencies": { "reg-suit": "^0.11.1" }, "devDependencies": { "reg-keygen-git-hash-plugin": "^0.11.1",

    "reg-notify-github-plugin": "^0.11.1", "reg-publish-gcs-plugin": "^0.11.1" }, "scripts": { "regression": "reg-suit run" } } CI SFHTVJUΛ࣮ߦ͢ΔͨΊͷઃఆΛ௥Ճ
  34. version: 2.1 orbs: android: circleci/[email protected] flutter: circleci/[email protected] node: circleci/[email protected] jobs:

    vrt: executor: android/android steps: - checkout - node/install: install-yarn: true - flutter/install_sdk: - run: name: Install melos command: | dart pub global activate melos melos run pub:get - run: name: Run Golden Testing command: melos run test:snapshot - run: name: Install dependencies command: yarn - run: name: Run VRT command: yarn regression workflows: vrt: jobs: - vrt CI
  35. @override Widget build(BuildContext context) { return Container( color: Colors.amberAccent, child:

    Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.star), SizedBox(width: 16), Text(text, style: Theme.of(context).textTheme.headline5) ], ), ); } ςετ
  36. @override Widget build(BuildContext context) { return Container( color: Colors.amberAccent, child:

    Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.star), SizedBox(width: 16), Text(text, style: Theme.of(context).textTheme.headline5) ], ), ); } ςετ ෆ۩߹͔൑அͮ͠Β͍