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

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

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

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

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();