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

Flutterで単体テストを行う方法とGitHub Actionsを使った自動化

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

Flutterで単体テストを行う方法とGitHub Actionsを使った自動化

Flutterでの単体テスト実施方法と、テスタブルなコードにリファクタリングするためのテクニック.
またそれをGitHub Actionsで自動化してカバレッジを可視化する方法

Avatar for tokku5552

tokku5552

May 11, 2022
Tweet

More Decks by tokku5552

Other Decks in Programming

Transcript

  1. Flutterにおけるテストの種類 Flutterには3種類のテストがある 公式ページ:https://flutter.dev/docs/cookbook/testing ・Unit Test    ・Widget Test  ・Integration Test いわゆる単体テスト。関数、メソッド、クラスの検証を行う

    Widgetが正しく生成されるかのテスト。 結合テスト。シナリオを書いてエミュレータ上で自動操作によるテス トが行える。 今回は主に、実装コストと効果の バランスが一番よさそうな Unit Testを扱う
  2. Unitテストの書き方と実行方法 プロジェクトルートの testフォルダの下に XXX_test.dartファイルを作成 import 'package:flutter_test/flutter_test.dart' ; import 'package:todo_app_sample_flutter/data/todo_item.dart' ;

    void main() { group('TodoItemのゲッターのテスト ', () { final TodoItem todoItem = TodoItem( id: 0, title: 'title', body: 'body', createdAt: DateTime (2020, 1, 1), updatedAt: DateTime (2020, 1, 1), isDone: true, ); test('idのテスト', () { expect (todoItem.getId, 0); });
  3. Unitテストの書き方と実行方法 プロジェクトルートの testフォルダの下に XXX_test.dartファイルを作成 import 'package:flutter_test/flutter_test.dart' ; import 'package:todo_app_sample_flutter/data/todo_item.dart' ;

    void main() { group('TodoItemのゲッターのテスト ', () { final TodoItem todoItem = TodoItem( id: 0, title: 'title', body: 'body', createdAt: DateTime (2020, 1, 1), updatedAt: DateTime (2020, 1, 1), isDone: true, ); test('idのテスト', () { expect (todoItem.getId, 0); }); main関数の中に 実際のテストを記載 test(‘テストケース名’,(){  実際のテスト処理  expect(結果,期待する値); }); group()でテストケースを まとめることが出来る。
  4. ここからが本題 class MemoDetailModel extends ChangeNotifier { final FirebaseAuth auth =

    FirebaseAuth.instance; final FirebaseFirestore firestore = FirebaseFirestore.instance; Future addMemo() async { final memo = Memo( ~~ );    ~~何かデータ追加前にチェックしたりとか~~ final collection = firestore.collection('users'); final user = auth.currentUser; if (user != null) { collection.doc(user.uid).collection('memos').add({ 'title': memo.title, 'updatedAt': memo.updatedAt, 'happenedAt': memo.happenedAt, }); } notifyListeners(); } } よくありそうなChangeNotifierを 継承したドメインモデル ビジネスロジックを実装しているので 単体テストを行いたいが、 右のような状態ではテスト出来ない。 どこが問題?
  5. なぜテスト出来ない? ・FirebaseAuthやFirestoreの  インスタンスを生成している  ※main()内でイニシャライズが必要 ・addMemo()ではバリデーションや、  データ追加前の正当性チェックなどを  行っているが、同時に Firebaseの  通信処理も行ってしまっている class

    MemoDetailModel extends ChangeNotifier { final FirebaseAuth auth = FirebaseAuth.instance; final FirebaseFirestore firestore = FirebaseFirestore.instance; Future addMemo() async { final memo = Memo( ~~ );    ~~何かデータ追加前にチェックしたりとか~~ final collection = firestore.collection('users'); final user = auth.currentUser; if (user != null) { collection.doc(user.uid).collection('memos').add({ 'title': memo.title, 'updatedAt': memo.updatedAt, 'happenedAt': memo.happenedAt, }); } notifyListeners(); } }
  6. インターフェース ・メソッドの実装を強制する仕組み ・メソッドだけ定義して実際の処理は書かない →内部でDB、Firebase等の処理を書くこともない。 (依存しない) ・インターフェースを実装したクラスは、 インターフェースに定義されているメソッドを 実装しなければならない →メソッドだけを外部から見ると、全く同じ動きをする interface

    Hoge{ void doHoge(); } class HogeImpl implements Hoge{ @override public void doHoge() { // 実際の処理 } } インターフェース(Javaでの例) インターフェースを実装した例 ただしDartにはインターフェースが ないので、今回はabstract というのを使う ※implicit interfaceという便利な機能があるが 話がややこしくなるので割愛
  7. インターフェースの使い方 ・コンストラクタで  インターフェースを受け取る ・インターフェースにはメソッドの  決まりが書いてあるので、  記載のあるメソッドは  そのまま使うことが出来る class TodoItemDetailModel extends

    ChangeNotifier { TodoItemDetailModel ({ @required TodoItemRepository todoItemRepository , }) : _todoItemRepository = todoItemRepository ; final TodoItemRepository _todoItemRepository ; Future <void> add() async { if (todoTitle == null || todoTitle .isEmpty) { final Error error = ArgumentError ('タイトルを入力してください。 '); throw error; } await _todoItemRepository .create( ~~ ); notifyListeners (); } https://github.com/tokku5552/TODOAppSample-Flutter/blob/v1.3/lib/presentation/todo_item_detail/todo_item_detail_model.dart
  8. インターフェースの使い方 ・コンストラクタで  インターフェースを受け取る ・インターフェースにはメソッドの  決まりが書いてあるので、  記載のあるメソッドは  そのまま使うことが出来る class TodoItemDetailModel extends

    ChangeNotifier { TodoItemDetailModel ({ @required TodoItemRepository todoItemRepository , }) : _todoItemRepository = todoItemRepository ; final TodoItemRepository _todoItemRepository ; Future <void> add() async { if (todoTitle == null || todoTitle .isEmpty) { final Error error = ArgumentError ('タイトルを入力してください。 '); throw error; } await _todoItemRepository .create( ~~ ); notifyListeners (); } https://github.com/tokku5552/TODOAppSample-Flutter/blob/v1.3/lib/presentation/todo_item_detail/todo_item_detail_model.dart 外部通信など(DBやFirebaseを使う処理)はリ ポジトリーというクラスに集約して、 ビジネスロジックから切り離す ビジネスロジックはインターフェースに 依存する。(具体的な実装に依存しない。)
  9. テスト側で使う例 ・テスト用のrepositoryを  直接インスタンス化する ・それを直接modelに渡す ・テストの中でrepositoryを  直接操作することが可能 void main() { final

    repository = TodoItemRepositoryMemImpl (); final model =   TodoItemDetailModel (todoItemRepository: repository ); final dummyDate = DateTime.now();