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

Flutterでもテスト駆動したい

 Flutterでもテスト駆動したい

Takashi Makino

May 19, 2023
Tweet

More Decks by Takashi Makino

Other Decks in Programming

Transcript

  1. Takashi Makino makky0620  未来シェア Overview About me  未来シェア 最適化っぽいコードを書いている ただ、フロントもAPIも⾊々やる!

     公⽴はこだて未来⼤学 ⺟校‧北海道函館市にある情報系の⼤学 今回のLT会を企画するきっかけに ⼭の上にあって⾃転⾞で⾏くのは⼤変  キャンプ 最近キャンプしてきて趣味にしたいと思っている 焚き⽕と飯が最⾼ Engineering Recent memory
  2. Takashi Makino makky0620  未来シェア Overview Popular language or frameworks  Python

    さくっと書きたい時に使う 仕事ではちょっとしたスクリプト作成 趣味では機械学習で使⽤  Java(Spring Boot) はじめましては⼤学1年⽣ Spring Bootをはじめたのは仕事をはじめてから オブジェクト指向とStreamAPIが好き  TypeScript(React) ⼤学時代のバイトで使いはじめた(当時はJS) フロントアプリを作るといえばこいつ 最近あまり書いてない  Dart(Flutter) 2023年1⽉から始めた(2018年に⼀度挫折) 仕事でネイティブアプリが作りたくなった 徐々にわかってきた気がしたので今⽇発表 Engineering
  3. 具体的にどうやるの? 受け取った⽂字列が数値かどうか判定する関数を作りたい void main() { test(ʻ数値である時trueを返す’, () { expect(isNumeric(ʻ123’), true);

    }); // テストを追加 test(ʻ数値でない時falseを返す’, () { expect(isNumeric(ʻhoge’), false); }); } bool isNumeric(String text) { return true; } // テストコード // プロダクションコード
  4. 具体的にどうやるの? 受け取った⽂字列が数値かどうか判定する関数を作りたい void main() { test(ʻ数値である時trueを返す’, () { expect(isNumeric(ʻ123’), true);

    }); test(ʻ数値でない時falseを返す’, () { expect(isNumeric(ʻhoge’), false); }); } bool isNumeric(String text) { bool isNumeric = false; try { int number = int.parse(text); isNumeric = true; } catch (e) { isNumeric = false; } return isNumeric; } // テストコード // プロダクションコード
  5. 具体的にどうやるの? 受け取った⽂字列が数値かどうか判定する関数を作りたい void main() { test(ʻ数値である時trueを返す’, () { expect(isNumeric(ʻ123’), true);

    }); test(ʻ数値でない時falseを返す’, () { expect(isNumeric(ʻhoge’), false); }); } bool isNumeric(String text) { return int.tryParse(text) != null; } // テストコード // プロダクションコード
  6. DIとは? 必要なものを外部から⽤意するような設計パターン class UserService { final UserRepository _repository; UserService(this._userRepository); Future<User>

    getUser(String id) async { return await _repository.fetchBy(id); } } void main() async { final repository = UserRepository(); final service = UserService(repository); final user = await service.getUser(ʻ123’); } class UserService { final UserRepository _repository; UserService() { _repository = UserRepository(); }; Future<User> getUser(String id) async { return await _repository.fetchBy(id); } } void main() async { final service = UserService(); final user = await service.getUser(ʻ123’); } // DI使⽤ // DI未使⽤
  7. DIを使うとテストが書きやすくなる? 依存関係をモックしてテストができる class UserService { final UserRepository _repository; UserService(this._userRepository); Future<User>

    getUser(String id) async { return await _repository.fetchBy(id); } } void main() async { final repository = UserRepository(); final service = UserService(repository); final user = await service.getUser(ʻ123’); } class UserRepositoryMock { Future<User> fetchBy(String id) async { return User(id: id); } } void main() { final mockRepository = UserRepositoryMock(); final service = UserService(mockRepository); test(ʻ指定されたidのユーザを返すこと’, () async { final actual = service.getUser(ʻ123’); expect(actual, User(id: ʻ123’)); }); } 依存関係をテストから分離 // テストコード
  8. RiverpodでDI final userViewModelProvider = StateNotifierProvider.autoDispose((ref) => UserViewModel(...)) class UserViewModel StateNotifier<...>

    { ... Future<void> load() async { final users = await repository.fetchAll(); state = UserState(users: users); } } class UserPage extends HooksConsumerWidget { const UserPage({Key? key}): super(key: key); @override Widget build(BuildContext context, WidgetRef ref) { final viewModel = ref.read(userViewModelProvider.notifier) return Scaffolding(...); } } Providerを⽤いてDIコンテナに登録し、WidgetRef.readを⽤いて依存性注⼊ 依存性注⼊ DIコンテナ登録
  9. mockitoでモックしてテスト class UserRepositoryMock { Future<User> getUser(String id) async { return

    User(id: id); } } void main() { final mockRepository = UserRepositoryMock(); final service = UserService(mockRepository); test(ʻ指定されたidのユーザを返すこと’, () async { final actual = await service.getUser(ʻ123’); expect(actual, User(id: ʻ123’)); }); } @GenerateMocks([UserRepository]) void main() { final mockRepository = MockUserRepository(); final service = UserService(mockRepository); test(ʻ指定されたidのユーザを返すこと’, () async { when(mockRepository.getUser(ʻ123’)) .thenAnswer((_) => Future.value(User(id: ʻ123’))); final actual = await service.getUser(ʻ123’); verify(mockRepository.getUser(ʻ123’)).called(1); expect(actual, User(id: ʻ123’)); }); } // mokito未使⽤ // mokito使⽤ whenで戻り値の設定、verifyでモックインスタンスの呼び出し検証ができる
  10. 右下のボタンどうしようかなぁ → FloatingActionButtonで実装しよう 遷移後の画⾯の名前どうしようかな → TaskEditPageにしよう テスト駆動開発を実践 void main() {

    testWidgets(ʻテストケース名', (tester) async { // 対象画⾯の描画 await tester.pumpWidget(ProviderScope( child: const MaterialApp(home: HomePage()))) // ボタンを押す await tester.tap(find.byType(FloatingActionButton)); await tester.pumpAndSettle(); // 新規作成画⾯への遷移を検証 expect(find.byType(TaskEditPage), findsOneWidget); }); } 「右下のボタンを押した時、新規作成画⾯が表⽰されること」をテストする
  11. Todoの情報はどこから持ってくる? → Repository層から持ってくる どこに表⽰させようかな → TaskItemウィジェットにを作ってそ の中に表⽰しよう テスト駆動開発を実践 void main()

    { testWidgets(ʻテストケース名', (tester) async { // Todo項⽬のモック var task = Task( when(taskRepository.fetchAll()) .thenAnswer((_) => Future.value([task])); // 対象画⾯の描画 await tester.pumpWidget(ProviderScope( child: const MaterialApp(home: HomePage()))) await tester.pumpAndSettle(); // タイトルが表⽰されていることの検証 var titleFinder = find.descendant( of: find.byType(TaskItem), matching: find.text(ʻタイトル’) ); expect(find.byType(TaskEditPage), findsOneWidget); }); } 「Todoの項⽬があった時、タイトルが表⽰されていること」をテストする 最⼩限の設計+品質の作り込み
  12. 今⽇のキーワード • テスト駆動開発:テストから書くことで品質の⾼いコードを作る開発技法 • DI(依存性注⼊):テストしやすくするための設計パターン Flutterでテスト駆動開発するために • riverpod:DI機能を提供してくれるパッケージ • mockito:モックオブジェクトが作れるようになるパッケージ

    所感(フロントのテストを書き慣れてない) • 親‧⼦ウィジェットでのテストケースの棲み分けがわからないなぁ 今⽇のコード:https://github.com/makky0620/flutter-demo まとめ:テストから書くのはいいぞぉ