Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
Search
tokku5552
May 11, 2022
Programming
1
83
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化
Flutterでの単体テスト実施方法と、テスタブルなコードにリファクタリングするためのテクニック.
またそれをGitHub Actionsで自動化してカバレッジを可視化する方法
tokku5552
May 11, 2022
Tweet
Share
More Decks by tokku5552
See All by tokku5552
Google CloudとAWSのコンテナ実行環境比較
tokku5552
0
140
高スループット・低レイテンシを実現する技術
tokku5552
3
8.2k
AWS CDKのススメ
tokku5552
1
450
Messaging APIのメッセージオブジェクトを検証できるChrome拡張機能を作った話
tokku5552
1
110
FlutterにLINEログインを仕込んで通知メッセージを送る
tokku5552
2
860
AWS CDK × Reactでliffをつくる
tokku5552
1
480
ネットワーク基礎 - WEBページが表示されるまで
tokku5552
1
230
インフラエンジニアのお仕事(オンプレ)
tokku5552
0
110
hooks riverpod + state notifier + freezed でのドメイン駆動設計
tokku5552
0
340
Other Decks in Programming
See All in Programming
どうして僕の作ったクラスが手続き型と言われなきゃいけないんですか
akikogoto
1
120
TypeScriptでライブラリとの依存を限定的にする方法
tutinoko
3
710
Outline View in SwiftUI
1024jp
1
340
flutterkaigi_2024.pdf
kyoheig3
0
150
CSC509 Lecture 12
javiergs
PRO
0
160
受け取る人から提供する人になるということ
little_rubyist
0
250
最新TCAキャッチアップ
0si43
0
200
2024/11/8 関西Kaggler会 2024 #3 / Kaggle Kernel で Gemma 2 × vLLM を動かす。
kohecchi
5
950
Functional Event Sourcing using Sekiban
tomohisa
0
110
聞き手から登壇者へ: RubyKaigi2024 LTでの初挑戦が 教えてくれた、可能性の星
mikik0
1
140
ペアーズにおけるAmazon Bedrockを⽤いた障害対応⽀援 ⽣成AIツールの導⼊事例 @ 20241115配信AWSウェビナー登壇
fukubaka0825
6
2k
EMになってからチームの成果を最大化するために取り組んだこと/ Maximize team performance as EM
nashiusagi
0
100
Featured
See All Featured
GitHub's CSS Performance
jonrohan
1030
460k
Facilitating Awesome Meetings
lara
50
6.1k
For a Future-Friendly Web
brad_frost
175
9.4k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
6
430
Bash Introduction
62gerente
608
210k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
28
9.1k
Building Your Own Lightsaber
phodgson
103
6.1k
Embracing the Ebb and Flow
colly
84
4.5k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
38
1.8k
A Philosophy of Restraint
colly
203
16k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
226
22k
Building Adaptive Systems
keathley
38
2.3k
Transcript
Flutterで単体テストを行う方法とGitHub Actionsを使った自動化 KBOYのFlutter大学 勉強会 2021/02/24
はじめに このスライドの内容は以下の記事をかみ砕いて説明するものになります。 ・Flutterで単体テストを書く : https://qiita.com/tokkun5552/items/ede8460bef4892f48e37 ・【Flutter】Providerで最低限のDIを行ってテスタブルなコードにリファクタリングする : https://qiita.com/tokkun5552/items/7af34769104e94f50745 ・【Flutter】GitHubActionsでテストと静的解析を自動化する https://qiita.com/tokkun5552/items/2eb6793501c152dabf33
・サンプルコードの GitHubリポジトリ tokku5552/TODOAppSample-Flutter: A sample Todo App with Provider
なぜテストするのか 結論:ソフトウェアの品質を高めるため ソフトウェアの品質が下がる要因 ・人間が書いているので間違いや考慮漏れは起こる→仕方がないこと ・ある場所に変更を加えることで、別の場所に影響を及ぼしバグが生まれる テストが品質を上げる理由 ・考慮漏れや間違いが起きにくくなる(客観的に見ることが出来る) ・変更時にテストして、動きが変わっていないことを確かめる
品質向上とリリースのスピードはトレードオフ 「Done is better than perfect」 → 完璧を目指すよりもまず終わらせろ とはいえ全く動かさずにリリースするわけにはいかない 品質の落としどころ ・ベータ版としてリリース → ユーザにテストしてもらう ・モンキーテスト → テストケースを決めずに適当にクリックしたり入力したりしてみる
選択肢として ビジネスロジックの自動テストを考えてみる
Flutterでのテストことはじめ
Flutterにおけるテストの種類 Flutterには3種類のテストがある 公式ページ:https://flutter.dev/docs/cookbook/testing ・Unit Test ・Widget Test ・Integration Test いわゆる単体テスト。関数、メソッド、クラスの検証を行う
Widgetが正しく生成されるかのテスト。 結合テスト。シナリオを書いてエミュレータ上で自動操作によるテス トが行える。
Flutterにおけるテストの種類 Flutterには3種類のテストがある 公式ページ:https://flutter.dev/docs/cookbook/testing ・Unit Test ・Widget Test ・Integration Test いわゆる単体テスト。関数、メソッド、クラスの検証を行う
Widgetが正しく生成されるかのテスト。 結合テスト。シナリオを書いてエミュレータ上で自動操作によるテス トが行える。 今回は主に、実装コストと効果の バランスが一番よさそうな Unit Testを扱う
UnitTestの準備 ・パッケージの導入 pubspec.yamlにflutter_testが追加されていること ※実際はtestパッケージがあれば良いが、 flutter_testパッケージに含まれる
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); });
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()でテストケースを まとめることが出来る。
テスト実行 1. テストファイルを右クリック 2. 実行→tests in XXXX デバッグの画面が開いて 結果が表示される
ここからが本題 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を 継承したドメインモデル ビジネスロジックを実装しているので 単体テストを行いたいが、 右のような状態ではテスト出来ない。 どこが問題?
なぜテスト出来ない? ・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(); } }
テストを行いやすくするために ・初期化が必要な外部パッケージ(DBやFirebaseなど)を 直接使っているclassはテストしにくい。 ※できないわけではない: cloud_firestore_mocks (https://pub.dev/packages/cloud_firestore_mocks) ・でもビジネスロジックなので、データをUI等から受け取って、 加工して追加・更新 という処理になるのは必至では・・・?
テストを行いやすくするために ・初期化が必要な外部パッケージ(DBやFirebaseなど)を 直接使っているclassはテストしにくい。 ※できないわけではない: cloud_firestore_mocks (https://pub.dev/packages/cloud_firestore_mocks) ・でもビジネスロジックなので、データをUI等から受け取って、 加工して追加・更新 という処理になるのは必至では・・・? 解決策:分離して疎結合にする!
ソフトウェアアーキテクチャの話
ビジネスロジックの集約と依存関係の分離 ・例えばUIのクラスにすべてのロ ジックを詰め込んで実装すること もできるが、それをやってしまうと 一つのクラスが肥大化してしまう ・肥大化したクラスはメンテナンス しづらく、読みにくく、テストしにく い Robert
C. Martin著 Clean Architecture 達人に学ぶソフトウェアの構造と設計
ビジネスロジックの集約と依存関係の分離 ・例えばUIのクラスにすべてのロ ジックを詰め込んで実装すること もできるが、それをやってしまうと 一つのクラスが肥大化してしまう ・肥大化したクラスはメンテナンス しづらく、読みにくく、テストしにく い Robert
C. Martin著 Clean Architecture 達人に学ぶソフトウェアの構造と設計 テスタブルなコードにするために 考えを少しだけ取り入れることにする
オブジェクト指向の話 ・オブジェクト指向とは何か?と言う話はしません ・オブジェクト指向っぽいプログラミングのテクニックを少し使って 今までテストできなくなってしまっていたコードを リファクタリングすることに注力します キーワード:インターフェース、実装、抽象クラス、継承 【Flutter】Providerで最低限のDIを行ってテスタブルなコードにリファクタリングする https://qiita.com/tokkun5552/items/7af34769104e94f50745
インターフェース ・メソッドの実装を強制する仕組み ・メソッドだけ定義して実際の処理は書かない →内部でDB、Firebase等の処理を書くこともない。 (依存しない) ・インターフェースを実装したクラスは、 インターフェースに定義されているメソッドを 実装しなければならない →メソッドだけを外部から見ると、全く同じ動きをする interface
Hoge{ void doHoge(); } class HogeImpl implements Hoge{ @override public void doHoge() { // 実際の処理 } } インターフェース(Javaでの例) インターフェースを実装した例
インターフェース ・メソッドの実装を強制する仕組み ・メソッドだけ定義して実際の処理は書かない →内部でDB、Firebase等の処理を書くこともない。 (依存しない) ・インターフェースを実装したクラスは、 インターフェースに定義されているメソッドを 実装しなければならない →メソッドだけを外部から見ると、全く同じ動きをする interface
Hoge{ void doHoge(); } class HogeImpl implements Hoge{ @override public void doHoge() { // 実際の処理 } } インターフェース(Javaでの例) インターフェースを実装した例 ただしDartにはインターフェースが ないので、今回はabstract というのを使う ※implicit interfaceという便利な機能があるが 話がややこしくなるので割愛
インターフェースの使い方 ・コンストラクタで インターフェースを受け取る ・インターフェースにはメソッドの 決まりが書いてあるので、 記載のあるメソッドは そのまま使うことが出来る 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
インターフェースの使い方 ・コンストラクタで インターフェースを受け取る ・インターフェースにはメソッドの 決まりが書いてあるので、 記載のあるメソッドは そのまま使うことが出来る 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を使う処理)はリ ポジトリーというクラスに集約して、 ビジネスロジックから切り離す ビジネスロジックはインターフェースに 依存する。(具体的な実装に依存しない。)
では、実際の処理はどう呼ばれる?
依存関係とDIの話 ・ここで登場するのがDI(Dependency Injection) よく「依存性の注入」と訳されるが、 「依存オブジェクトの注入」と考えた方が分かりやすい。 ・先の例のように依存するオブジェクトを コンストラクタなどで外から受け取るように実装する
依存関係とDIの話 TodoItemDetailModel field TodoItemRepository Interface TodoItemRepository TodoItemRepositoryImpl 依存 注入 実装
ProviderでDIする ・注入の処理をまとめて書くためのクラスをDIコンテナーと言う ・Flutterの場合get_itというDIコンテナーがある get_it | pub.dev : https://pub.dev/packages/get_it ・get_itを使っても良いが、Providerの機能でもDIのようなことは出来るので、 今回はProviderを使う
ProviderでDIする ・例えばmainの直下などで、 右図のように定義する ・providersの中はProviderでも、 StreamProviderでも、 ChangeNotifierProviderでも 定義できる。 ・create:でImplの方を渡す void main()
{ runApp( MultiProvider ( providers: [ Provider <TodoItemRepository >( create: (_) => TodoItemRepositoryImpl (), ) ], child: App (), ), ); }
ProviderでDIする ・modelを生成する場所(例えば page内のChangeNotifierProvider) でコンストラクタに渡す。 ・先ほどmainで定義した Providerは、定義した場所より 下の階層のcontextから context.read<型>() で呼び出せる ChangeNotifierProvider
<TodoItemDetailModel >( create: (_) => TodoItemDetailModel ( todoItemRepository: context .read<TodoItemRepository >(), ), Providerの詳細 参考:https://qiita.com/kabochapo/items/a90d8438243c27e2f6d9
Flutterにおけるcontext ・FlutterはWidgetをツリー状に連ねて書いていく ・そのツリーの中で自分が今いる場所を表す ・HogeWidgetのbuildで引数として受け取るBuildContextには Scaffoldやそれより先祖の情報は入っているが、 FugaWidgetの情報は入ってない ・今回の例だとcontext.read<>()を記載するよりも 先祖でProviderを呼ばなければいけない main Material
App Scaffold Hoge Widget FugaW idget
DIを行うと、テストの時に差し替えられる ・アプリで使用するリポジトリの実装 と、テストでの実装を別々に書ける ・test側の実装は、DBを呼ばず インメモリのDBとして記述する ・XXX_test.dartの中で直接渡せばよい
テスト用Repositoryの実装の例 ・DBのインスタンスは生成しない ・実際のデータは_data ・アプリ側と同様に TodoItemRepositoryを実装する class TodoItemRepositoryMemImpl implements TodoItemRepository {
final _data = <int, TodoItem>{}; ~~ @override Future<TodoItem> find({@required int id}) { return Future.value(_data[id]); } ~~
テスト側で使う例 ・テスト用のrepositoryを 直接インスタンス化する ・それを直接modelに渡す ・テストの中でrepositoryを 直接操作することが可能 void main() { final
repository = TodoItemRepositoryMemImpl (); final model = TodoItemDetailModel (todoItemRepository: repository ); final dummyDate = DateTime.now();
テスタブルなコードにするまとめ ・テストを行いたいclassから依存オブジェクトを排除する。 例:Firestore 、 FirebaseAuth、sqflite 、SharedPreferences ・上記のようなものを扱うRepositoryを定義して、 インターフェースと実装に分ける ・Providerでrepositoryの実装をコンストラクタで渡す ・テスト用のrepositoryを書く
テスト実行を自動化しよう
テスト自動化のメリット ・ローカルで毎回テストしてもいいが面倒くさい ・テストの結果がGitHub上に残る ・後になってバグが見つかった時に、どの時点まで正しかったのか追える ・プルリクをレビューする際の目安になる ・ビジネスロジックのレビューは単体テストだけ見れば正しいかどうかわかる ・ロジックに考慮漏れがあれば、レビュアーがテストを追加して 担当者がテストを通過できるように修正する とかも可能
GitHub Actionsでテスト自動化 ・プロジェクト直下に.github/workflowsというフォルダを作成し、その中にyamlファイル を置いていきます。 テスト用yamlファイル↓ yamlの詳しい解説↓ flutter test の実行を行ってる https://qiita.com/tokkun5552/items/2eb6793501c152dabf33#%E3%83 %86%E3%82%B9%E3%83%88%E8%87%AA%E5%8B%95%E5%8C%96
https://qiita.com/sensuikan1973/items/0c5efb93e5db54f9d8a2
None
None
None
Codecovやlcovでカバレッジの可視化 ・右画像のように、テスト出来ていない ところが分かる ・やり方は以下 https://qiita.com/tokkun5552/items/2eb6793501c152dabf 33#codecov%E3%81%A7%E3%82%AB%E3%83%90%E3%83 %AC%E3%83%83%E3%82%B8%E5%8F%AF%E8%A6%96%E5 %8C%96 ・実際の画面を見てみましょう
まとめ ・単体テストの基本的な書き方と実行の仕方が分かった ・modelから外部処理をRepositoryに分離する方法が分かった ・RepositoryをProviderでDIする方法が分かった ・GitHub Actionsでテストを自動実行する方法が分かった ・Codecovでカバレッジを可視化する方法がわかった ご清聴ありがとうございました