Flutterのテストは、ウィジェットレベルだけでよくね
Flutterには3段階のテストパターンが存在する。
- Unit Test
- Widget Test
- Integration Test
アプリケーションの性質にもよるだろうけど、ローカル or HTTPでデータをフェッチして表示するというロジックが多くを占める場合、Unit Testでテストすることがかなり少ない。Model側にビジネスロジックがある場合(数の集計)ぐらいかなぁ。なので、UnitTestを書くシーンがあまりない。
購買するようなアプリケーションの場合、ユーザー目線だと、以下の手順を踏む。
- 商品を一覧から選ぶ(画面A)
- 数量を決めて追加を押す(画面B)
- お買い物かごに表示される(画面C)
WidgetTestになると、こういう感じかなぁ。
- 画面A
- 商品の一覧がリスト表示され、意図した箇所に情報が出ていること
- スクロールしたらアイテムが追加されること
- pull to refreshができること
- タップしたらB画面に遷移する
- 画面B
- 必要な情報が意図したところに出ていること
- 数量が増えたり減ったりすること
- pull to refreshができること
- タップしたらC画面に遷移する
- 画面C
- 必要な情報が意図したところに出ていること
- アイテムの増減に伴い合計額などが変動すること
ウィジェットの動作が担保できているなら、頑張ってインテグレーション・テストを書くコストを取るべきなのか。それとも、インテグレーション・テストを頑張って書くのが良いのか。
・・・って思ったけど、ここで重要なのはE2Eの担保じゃなくて、ウィジェットの動きの担保だから、ウィジェット・テストで充分な気がするな。
Flutter(iOS)でアプリがアンインストールされたかを知る
キーチェーンに書き込んだ情報はアプリがアンインストールされても残ってしまうので、ユーザーがアプリを再インストールした時にそのtokenが残ってちょっとやだ、みたいなケース。
結論から言うと、NSUserDefaultsに書き込んで逃げるしか無いっぽい。アプリがアンインストールされたら消えるローカル情報、ここしか無い。
Flutterだとこれ。やってること一緒。
Flutterで任意の画面までPopUntilしたい
- 商品検索
- 商品詳細
- 会員登録 or ログイン
- ログインページ
こういうページ遷移の時、ログインページでログインが完了したら、2番の商品詳細に戻したい場合のTipsです。
2の商品詳細は、1の親画面でタップされたデータを元に画面を初期化するので、予めroutes:
に画面遷移を登録できないので、MaterialPageRoute
を毎回呼び出して画面遷移をしています。
PopUntil
で戻るためには、route.settings
に遷移履歴をセットしておかないと任意の画面まで戻れないので、以下のようなコードを書いて都度RouteSetting
を指定します。これで戻れます。
遷移する時
Navigator.of(context).push(MaterialPageRoute( //これ settings: RouteSettings(name: ItemDetailPage.routeName), builder: (context) => ItemDetailPage(item: item), ));
戻る時
Navigator.of(context).popUntil((route) => route.settings.name == ItemDetailPage.routeName);
良く出来てるよ、Flutterって。ネイティブアプリを作る面倒さを味わっているから、本当に楽しくネイティブアプリが書ける。設計思想も勉強になる。
大量のライブラリの面倒を見てくれるFlutterのありがたみ
Flutterを簡単に言えば、Dartでアプリを作るとiOS/Androidのネイティブアプリのプロジェクトに翻訳して、ネイティブアプリをワンソースで出来る仕組みです。
その中で、Flutterが提供してくれる多くのウィジェットや通信・DBのライブラリなどがあったりするんですが、それらはDartで書かれているわけではなく、ネイティブの言語で書かれたライブラリを使っています。OrganizerでアプリをAppStoreConnectに送るとよく分かる。Cocoapodsで外部ライブラリを色々Includeしてる。
これらのネイティブで動作するライブラリをFlutterの公式がサポートしてくれているって、すごいことだよな。。。こんなにありがてぇことはないし、ちょっと怖くなるよ。。
Friendlyで「出来たらいいな」2つ
WPFアプリのE2Eテストを書く必要があり、Friendlyを触ってみた。
ishikawa-tatsuya.hatenablog.com
2つほど出来たらいいな、があった。
1. ViewModelのオブジェクトを取りたい
//WindowControl w var vm = w.AppVar.Dynamic().DataContext(); if(vm is HogeViewModel) { //falseになる }
ってやると、dynamicで対象のビューのVMが取れるし、メソッドを叩くこともできる。ただ、VMの型にキャストすることが出来ない。VMの型にキャストできれば、Assertするのが楽になるので、やりたい。
2. HierarchicalTemplate
こちらに書いたが、メニューの子メニューをVisualTreeで辿れない。
とりあえず、書き散らかしただけですいません。。。
追記(2020.12.11)
vmへのキャストについてですが、別途[Serializable]
を適用しないと、だめっぽい。
WPFGridのdataitemを取るのに、string code = ListGrid[1].itemcode
とかやれば、stringで取れる。このstringで代入するときに、dynamicから型推論で型を解決するようなのだが、これがシリアライズによって値を持ってくるっぽいの。dynamicの機構のようだ。(怪しいけど、代入を実行することで、相手プロセスから自分のアプリのプロセスに展開されるっぽい)
HierarchicalTemplateのたどり方は、相変わらずうまくいかなかったが、ViewModelを叩くことが出来るので、メニューのUIを捕まえるのではなくViewModelのコマンドをそのまま実行して、ま、とりあえずなんだけど、テストケースを実行することは出来た。メニューや右クリックメニュー等は、Windowsのネイティブのオブジェクトらしくて、色々大変なようだ。
右クリックメニューを捕まえるの苦労したが、VisualTreeでWPFUserControlをセットして、x:Nameで辿ったら取れた。
PageObject.InputGrid.MouseDown(MouseButtonType.Left, 100, 50); PageObject.InputGrid.MouseUp(MouseButtonType.Left, 100, 120); PageObject.InputGrid.Click(MouseButtonType.Right, 100, 100); var hoge = new WPFMenuItem(PageObject.ContentPage.Dynamic().StatusMenu); hoge.Click();
モーダルダイアログの操作もできた。モーダルの連鎖も、WaitForNextModal
を連発することで、動いた。
var async = new Async(); button.EmulateClick(async); var dlg = w.WaitForNextModal(); var dlg_po2 = new DialogPageObject(dlg); dlg_po2.OKButton.EmulateClick(); async.WaitForCompletion();
まず、PageObjectを作ろう
PageObjectは多分Seleniumの用語だと思うけど、テスト対象画面に存在するUIオブジェクトをVisualTreeから取得して変数に保存しておくこと。まず、これを当該画面を表示したら行う。PO初期化()
っていう関数を作って、ClassInitilize
で呼び出すようにしてます。対象のオブジェクトがないと例外を出してくれるので(ナイス!)、そこで動かない場合はそこから。これができれば、7割ぐらい出来たようなもの。
DeployGateのコマンドラインツールのインストールでコケた
こんなログが出た
"xcrun clang -o conftest -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/universal-darwin19 -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/backward -I/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0 -I. -D_XOPEN_SOURCE -D_DARWIN_C_SOURCE -D_DARWIN_UNLIMITED_SELECT -D_REENTRANT -g -Os -pipe -DHAVE_GCC_ATOMIC_BUILTINS conftest.c -L. -L/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib -L. -L/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.Internal.sdk/usr/local/lib -arch x86_64 -lruby.2.6 " In file included from conftest.c:1: In file included from /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby.h:33: /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: fatal error: 'ruby/config.h' file not found #include "ruby/config.h" ^~~~~~~~~~~~~~~ /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/include/ruby-2.6.0/ruby/ruby.h:24:10: note: did not find header 'config.h' in framework 'ruby' (loaded from '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks') 1 error generated. checked program was: /* begin */ 1: #include "ruby.h" 2: 3: int main(int argc, char **argv) 4: { 5: return 0; 6: } /* end */
Xcodeのコマンドラインツールのパスが違ってたので直したらインストールできた
$ sudo xcode-select -switch /Library/Developer/CommandLineTools
これだけ。
あらざ〜す
MacのhomebrewでOpenSSLがビルドエラーになる場合の対処方法|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社
RiverPodをやってみた
Flutterの設計で一番悩ましいのが、ViewとModelの連携。状態遷移だと思われる。
2020年の初頭からFlutterやりはじめて、「Provider + ChangeNotifier」でやってた。これでも全然動く。
ただ、Providerは必ずcontext
を経由する必要があるので、ウィジェットの階層構造に気をつけないといけないとか、データを変更した後にnotifyListeners
を自分で呼ばないといけないとか、そういう面倒さがあった。
値を再代入するだけで、当該UIのウィジェットが再ビルドされたらそれで良いのだけども、GUI(デスクトップアプリやスマホアプリ)を作る場合にそこが一番面倒。
RIverpodの便利なところは、提供されるProviderがimmutableに設計されているため、グローバルに定義しても良い点。BuildContextに依存しない点の2点がでかい。
riverpod版
import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:state_notifier/state_notifier.dart'; final counterProvider = StateNotifierProvider((_) => CounterNotifier()); class CounterNotifier extends StateNotifier<int> { CounterNotifier(): super(0); void increment () => state++; } class CounterPage extends HookWidget { Widget build(BuildContext context) { final v = useProvider(counterProvider.state); final counter = useProvider(counterProvider); return Scaffold( body: Center( child: Text(v.toString(), style: TextStyle(fontSize: 24)), ), floatingActionButton: FloatingActionButton( onPressed: () => counter.increment(), child: Icon(Icons.add), ), ); } }
StatefulWidget + State + setState
import 'package:flutter/material.dart'; class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => new _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState(() { _counter++; }); } @override Widget build(BuildContext context) { return new Scaffold( body: Center( child: Text('$_counter', style: TextStyle(fontSize: 24)) ), ), floatingActionButton: new FloatingActionButton( onPressed: _incrementCounter, child: new Icon(Icons.add), ), ); } }
この2つを見比べると、パッと見るだけでも以下のことがわかる。
- ユーザーの操作に伴い
setState
を書きまくるコードはだいぶ辛い。 - StateNotifierでは
state
を再代入するだけに徹する。公開される値がstate
しか無いのがミソ。- 再代入するとはいえ、State
の T
の持ってる色んなプロパティの状態をキープしつつイミュータブルにしたいので、freezedパッケージを使うのよね、と。
- 再代入するとはいえ、State
個人的には、notifyListener()
を意識しなくて良いのが好きなので、StateNotifier
を推していきたい。
- スクロールの管理
- テキストボックスの値の代入
- 引数ありの画面遷移
この辺がクリア(自分の中で噛み砕ければ)できれば、Riverpodいける。がんばろう。