Life is Really Short, Have Your Life!!

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

React x TypeScript、フィーリングッド

oukayuka.booth.pm

こちらのReact3部作を全部買ったんですが、読み終わったらメインにも書くけど、このシリーズは最高です。

近年はPythonをメインにすることが多く、TypeHintも真面目に使ってこなかった。Flutterやり始めてDartを使うようになり、Dartの癖のなさに助けられた。

Flutterにすんなり入れたのはDartのおかげだけど、裏返すとReact/Vueからは距離をとっていたのよね。モダンJavaScriptで知ってるのってアロー演算子ぐらいだし。どこかで再入門したいと思っていた。で、上記の本を買った。

おかげさまで、JavaScriptの言語仕様・関数型プログラミング、TypeScriptによる型安全など、モダンJavaScriptの基本を知ることができた。実際コードを書いてみると、すごく気持ちよく書けた。特に関数型。

型を安全に引き回すのは気持ちがいいし、関数をチェーンする書き方は慣れると宣言的で簡潔になる。スッキリする。単純に左から右へ評価されて最終的な値へ到達する宣言(式)を書くスタイルは、副作用も起きにくい。StatementとExpressionの違いを肌で感じられたのは、この本のおかげ。

3年ぐらい前にVueをやった時、正直あんまり楽しくなかったのよね。リアクティブなテンプレートエンジンを書かされている感で「ほほ〜こいつは新しいな〜」という刺激は薄かった。v-if っとか v-for とか覚えることが多いし。Flutterを覚えると、 item.map( (e) => Text(e)) 的な感じで、モデルのデータからWidgetをmapで引き回させてくれよって思う。そのニーズに応えてくれるのが、もちろんReactだ。

Reactは、Flutterのように(Flutterの始祖はReactだから当然だけど)ビューのレンダリングから状態管理までをコードで完結できるスタイルなので、これだよな〜と。ReactはモダンなJavaScriptが書けないと真価が出ない。全てがビューをレンダリングする関数の組み合わせで実装するという「関数型ストロングスタイル」に触れてみたら、非常に刺激的で楽しいものでした。これに慣れるとクラスベースのオブジェクト指向は無用の長物感がある。

というわけで、Diverseさんと同様に、今後はTypeScriptとDartの2つをメインにやっていきです。

Pythonを始めとして多言語でも関数型のアプローチは出来ると思うけど、FEを「React/Flutter」って決めたので、バックエンドもTypeScriptでよくねってなっちゃったのよねー

HerokuのJawsDBでmax_questions error

Herokuで借りているMySQL(JawsDB)で、2万件弱のSQLを発行する処理(データの一括登録)を投げてしまったら、以下のエラーが出た。

"User 'hogehogehoge' has exceeded the 'max_questions' resource (current value: 18000)")

devcenter.heroku.com

sharedプランの安いインスタンスなので、 1時間あたりに実行可能なクエリの数を超えてしまった模様。困ったことにこのエラーを踏んでしまうと、1時間経過しないとリセットされず、一切のSQLが実行できなくなってしまう。実質ダウンだよね。ドキュメントに明言されている。

The ‘max_questions’ value is a limit that is placed on shared plan accounts to help preserve computing power for other shared plan users on the same server. It is essentially a limit on the number of queries that can be executed from that account in an hour. After 1 hour of reaching the limit, the limit will be reset and queries can again be executed.

After 1 hour of reaching the limit, the limit will be reset and queries can again be executed.

なるほどね。

大量のSQLを投げる必要がある場合、1つのクエリにまとめないとだめだな。SQLAlchemyでadd_allしたとしても、SQL文がリストの数だけ作られるっぽい。MySQLクライアントとかで流し込んだほうが確実だが、それでいいのか俺。

2022年はフロントエンドの年になる(個人的に)

あけおめあけおめ。

2021年からFlutterマンになった。フロントエンドは、技術以外の課題にも向き合えるのが自分に向いている。サービスフローであったり、UXのあり方であったり。

Flutterは中級駆け出しにはなれたので、次はFlutterのマザーにあたるReactをやりつつ、モダンなWebブラウザベースのWebアプリケーションの作り方を一新しようと思う。Pythonは一旦忘れて、TypeScriptだけで書ききってみたい。そういうフルスタックフレームワークも出てきたし、メンテナー日本人だし。

Webアプリケーションを学び直す場合、フルスタックフレームワークに入門するのが効率が良いと思っている。開発全体の流れが網羅的につかめるのと、今時な組み合わせがわかる。API通信はきっとGraphQLだろうし、クライアントはNext.js、ORMもある。5年周期でだいたいかわるよね、こういうの。

バックエンドはPaaSが豊富にある。すごい便利。VPS立てるのアホみたいだもん。Render/Firebaseの組み合わせで生きていける気がする。AWSは使いこなせればすごく良いと思うけど、秒で数十しか来ないWebアプリのデプロイするだけなら、使い込む余地が少ない。ファイル関係は全部S3にアップロードすればいいだけだし。

  • SSH開けなくていい
  • コンテナがデプロイできる
  • オートスケールする
  • モニタリング/DBもついてくる
  • バッチ用にCronがある

TypeScriptとNext.js、覚えるぞ〜。

FlutterのWidgetTest攻略メモ(随時更新)

WidgetTestを書いていて、Riverpodでビューの操作を行うクラスを全部DIして、ProviderのOverrideでMockに差し替えることをやっている。Mockにはmocktailを使っていて、このテスト戦略はシンプルでよい。サクサクテストが書ける。

riverpod.dev

どうせ忘れるので、WidgetTestを書いてきて遭遇したエラー内容をメモる。RiverPodのバージョンはもちろん1.0だ!

FutureメソッドのMock

Flutter書いているとtype 'Null' is not a subtype of type 'Future<T>のエラーにであう。Futureを使っているロジックは、明示的にちゃんとMockで値を返すように設定しないとアカン。

今回はboolだったのでこうなった。any()はどんな引数でもええでというやつ。returnしないとnullが返るから気をつけよう。

どうせ違うのは戻りがあるかないかぐらいで、ProviderでDIするロジックってイベントハンドラなので基本voidだから、もうちょい簡単になるかも。

    // 戻り値あり、引数あり
    when(() => fakeService.doSomething(any())).thenAnswer((_) async { return Future<bool>.value(true);});
    // 戻り値なし、引数あり
    when(() => fakeService.doSomething(any())).thenAnswer((_) => Future.value());
    // 引数も戻り値もなし
    when(fakeService.doSomething).thenAnswer((_) => Future.value();});

ViewModelとして機能するStateNotifierにHTTPでAPI取ってくる処理までも内包していいのか、ちょっと悩んでる。今は外してる。つまり、Repositoryの役目を果たすクラスはRiiverPodのProvider、画面の入出力に必要な値をひとつのPageStateにまとめて、StateNotifierで管理してる。ま、これで動くからそれでいいかなぁ。

FutureProviderの上書き

FutureProviderで保有している型(ここではHogePageState)に対し、てきとーな値を入れておく。ローカルストレージ系のものは、ローカルストレージからXXを取得してオブジェクトに入れた前提で動かせば良いと思われる。

SharedPreferencesをProviderでキャッシュする作戦をとっている場合、SharedPreferencesをMockしたProviderを作るのがちょっと面倒かもしれん。getの引数でthenAnswer連打の印象。書いてないから違ったらごめんね。

      hogeFutureProvder.overrideWithValue(const AsyncValue.data(HogePageState(
         name: "hoge", type: "foo")))

No Media Query

MaterialAppで囲ってない。

Could not find a generator for route RouteSettings

Mockで適当に差し込んだMaterialAppにRoutesの定義がないので、画面遷移に失敗する。

Widgetの型を指定し、そこに含まれるテキストで検索する

widgetWithTextが俺たちにはあった。

    await tester.tap(find.text("Press"));
    await tester.pumpAndSettle();
    expect(find.widgetWithText(AlertDialog, "Dialog Pressed"), findsOneWidget);

findのAPI、全部見たほうがええな。

StateNotifierProviderのMockができない

こういうコードを書くとProviderExceptionが発生し、StateNotifierProvider#addListenertype 'Null' is not a subtype of type '() => void'というエラーが出る。voidの関数が入るべき所にNull入れるなよボケってところ。

class FakeFooStateNotifier extends Mock implements FooStateNotifier {}

  testWidgets('ItemPage Search Test', (WidgetTester tester) async {
    final fakeViewModel = new FakeFooStateNotifier();
    await tester.pumpWidget(ProviderScope(
        overrides: [
          fooState.overrideWithValue(fakeViewModel),
        ],
        child: MaterialApp(
          home: FooPage(),
        )));
  });

addListenerがnullだったら適当にvoid Function作ったろかでノリでwhen(() => fakeViewModel.addListener(any())).thenReturn(() => {});とか書くと、stateが与えられていないせいかBad state: Tried to read the state of an uninitialized providerという別の例外がでた。

1.0以前はfakeViewModel.state.overrideWithValue(mock)みたいな書き方ができたけど、1.0の場合はstateがprotectedなので、外からアクセスできない。

StateNotifier自体がMockできないので、stateを操作する以外のロジック(例えばローカルストレージ読み込み)を書いてはいけないと思われる。Providerのrefを使ってStateNotifierProviderを読み込ませ、そこで状態を変更しよう。StateNotifierとStateNotifierを操作するサービスクラスの2つに分けたほうがいいな。Mockで差し替わらないというのが最大の理由だけど。

同じ問題に直面した人がIssueを立てているけど、多分スルーされる気がする。

github.com

うっかりSharedPreferenceを読み込んだコードをMockしないで死亡

この場合、エラーも何も出ないのよね。Widgetテストの宿命かもしれないけど、テスト対象のウィジェットで例外吐いている内容を捕まえられない。デバッグモードなら行けるのかなぁ...

SharedPreferenceを参照してるコードをMockで差し替えるのを漏れていて、画面遷移に失敗しており、画面遷移するはずのテストにコケていた。慣れの問題だろうけど...

Widgetテスト書いてると、きちんと外部ソースとビューのレンダリングの責務が分けられるのは実感する。その意味でクリーンというか疎結合になる。

あと、Golden Test(事前のスクショとテスト操作後のスクショ比べるやつ)は、あんまり有効性を感じない。ビューのリグレッションを検知することより、インテグレーションテストで、外部ソースと適切に連携できているかテストするほうが、品質は高まる気がしているため。

DockerのSelenium-HQで「cannot create temp dir for user data dir in chrome driver」

DockerのSeleniumで、こんなエラーが出ました。

Unknown error: cannot create temp dir for user data dir in chrome driver

MacのDockerのディスクが32GBで、30GB使っていた。2GBあったらええんちゃうかと思ってたけど、足りないようで... MacのDockerのディスク容量を増やしたら解決しました。おしまい。

FlutterのWidgetテストをちゃんとできるような設計にするお勉強

FlutterのテストにはUnit / Widget / Integrationの3つの段階があるが、外部データソースのアクセスを利用しないでDartのオブジェクト上だけでテストを行うのが、UnitとWidgetのテスト。

とはいっても、昨今で外部リソースを一切使わないケースはない。

  • HTTPSAPI通信
  • FirebaseのRemoteConfig
  • SharedPrefrenceに代表されるOSが持つローカルストレージ
  • SQLiteのようなローカルDB

ただHTTP通信やローカルストレージをMockにした所で、ほとんど使えない。それらを内部で呼び出している関数(ロジック)をそっくりMockに差し替えることができる必要がある。もちろん、プロダクトで動くソースコードに手を加えることなく。ビューに外部データソースに関するロジックを埋め込むとウィジェットテストを行えないのだ。

このようなケースに対してどう対応するかだが、まぁ簡単に言えば、ビューから呼び出されているビジネスロジックをMockで上書きするしかないのでははかろうか。そのための仕組みとして、またまたRiverpodの出番となる。RiverpodにはProviderをオーバーライドできる機能があるのだ。

このソースを見た時に、ほおおおおおそういうことねぇえええと思った。

github.com

class MockNewsViewModel extends Mock implements NewsViewModel {}

void main() {
  final mockNewsViewModel = MockNewsViewModel();
  when(mockNewsViewModel.fetchNews).thenAnswer((_) => Future.value());
  when(() => mockNewsViewModel.news)
      .thenReturn(Result.success(data: dummyNews));

  testWidgets('App widget test', (tester) async {
    await tester.pumpWidget(
      ProviderScope(
        overrides: [
          newsViewModelProvider.overrideWithValue(mockNewsViewModel),
        ],
        child: const App(),
      ),
    );
  });

ViewModelに該当するクラスをそっくりそのままMockに差し替える。これはmocktailパッケージで提供される。なんでこんなことができるのかさっぱりわからないのが辛いので、いつかコードを読まないといけない。リフレクションに近いなにかだと思う。

差し替えたViewModelクラスに対して、適当な値を突っ込む。thenReturnとかthenAnswerがそれ。Mockだから決め打ちで良い。非同期で呼びされるメソッドの場合は、thenAnswerを使うようだ。staticなメソッドのMockはできないらしいが、インスタンスメソッド内でstaticメソッドが呼びされている場合は、どうなんだろう。多分大丈夫だろきっと。

ListViewのテストでは、hogeが2行目に表示されていることを捕まえる必要があり、下記にあるように、何行目のアイテムを捕まえるコードを書かないといけない。

stackoverflow.com

1つ困ったのが、SQLiteのMockテスト。今回、読み取り専用のマスタデータをsqlite形式で持ってる。検索パラメーターは2つある。この場合、決め打ちでthenAnswerしちゃったら、ビューの表示確認にはなるけど、SQLの組み立てのテストにはならない。SQLを文字列で組み立てるようなことをすると、テスタビリティが損なわれるんだな。他のやり方を考えよう。最悪はユニットテストレベルで担保すればいいや。

FutureProviderを持つ親画面を、子画面から更新する

Riverpodネタは続く。

カート一覧→タップで数量更新→画面が戻った時にカートが更新・・・されない!データは書き換わっているが、UIが更新されていなかった。

一覧画面のデータFutureProvider.autoDisposeを使って、画面下部のタブに表示している。iPhoneはわからないけど、Androidエミュレータで試したところ、親→子に遷移する時は、親のProviderはautoDisposeの対象にならないみたい。

このような場合は、強制的にFutureProviderを更新する必要がある。このAPIを使う。

pub.dev

使い方はベリーイージー。グローバルに定義している対象のproviderを引数に入れるだけ。

ref.refresh(yourProvider);

オチとしては、ローカルにデータを書き込む系の場合、たいてい非同期になっている。なので、awaitを入れて非同期の書き込み終了が完了してから(もしくはthenのコールバックで)refreshを呼び出さないとつまらない結果を生む。