Life is Really Short, Have Your Life!!

ござ先輩の主に技術的なメモ

Flutterのテストは、ウィジェットレベルだけでよくね

Flutterには3段階のテストパターンが存在する。

  • Unit Test
  • Widget Test
  • Integration Test

アプリケーションの性質にもよるだろうけど、ローカル or HTTPでデータをフェッチして表示するというロジックが多くを占める場合、Unit Testでテストすることがかなり少ない。Model側にビジネスロジックがある場合(数の集計)ぐらいかなぁ。なので、UnitTestを書くシーンがあまりない。

購買するようなアプリケーションの場合、ユーザー目線だと、以下の手順を踏む。

  1. 商品を一覧から選ぶ(画面A)
  2. 数量を決めて追加を押す(画面B)
  3. お買い物かごに表示される(画面C)

WidgetTestになると、こういう感じかなぁ。

  • 画面A
    • 商品の一覧がリスト表示され、意図した箇所に情報が出ていること
    • スクロールしたらアイテムが追加されること
    • pull to refreshができること
    • タップしたらB画面に遷移する
  • 画面B
    • 必要な情報が意図したところに出ていること
    • 数量が増えたり減ったりすること
    • pull to refreshができること
    • タップしたらC画面に遷移する
  • 画面C
    • 必要な情報が意図したところに出ていること
    • アイテムの増減に伴い合計額などが変動すること

ウィジェットの動作が担保できているなら、頑張ってインテグレーション・テストを書くコストを取るべきなのか。それとも、インテグレーション・テストを頑張って書くのが良いのか。

・・・って思ったけど、ここで重要なのはE2Eの担保じゃなくて、ウィジェットの動きの担保だから、ウィジェット・テストで充分な気がするな。

Flutter(iOS)でアプリがアンインストールされたかを知る

キーチェーンに書き込んだ情報はアプリがアンインストールされても残ってしまうので、ユーザーがアプリを再インストールした時にそのtokenが残ってちょっとやだ、みたいなケース。

結論から言うと、NSUserDefaultsに書き込んで逃げるしか無いっぽい。アプリがアンインストールされたら消えるローカル情報、ここしか無い。

stackoverflow.com

Flutterだとこれ。やってること一緒。

stackoverflow.com

Flutterで任意の画面までPopUntilしたい

  1. 商品検索
  2. 商品詳細
  3. 会員登録 or ログイン
  4. ログインページ

こういうページ遷移の時、ログインページでログインが完了したら、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してる。

f:id:gothedistance:20201215135327p:plain
Organizer

これらのネイティブで動作するライブラリを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で辿れない。

qiita.com

とりあえず、書き散らかしただけですいません。。。

追記(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しか無いのがミソ。
    • 再代入するとはいえ、StateTの持ってる色んなプロパティの状態をキープしつつイミュータブルにしたいので、freezedパッケージを使うのよね、と。

個人的には、notifyListener()を意識しなくて良いのが好きなので、StateNotifierを推していきたい。

  • スクロールの管理
  • テキストボックスの値の代入
  • 引数ありの画面遷移

この辺がクリア(自分の中で噛み砕ければ)できれば、Riverpodいける。がんばろう。