0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter/Riverpod】Generatorで自動生成した(Async)NotifierProviderをoverrideWithで上書き

Posted at

表題のやり方をピンポイントに解説した日本語情報が見つけられなかった(探し方が悪いだけかもしれませんが)ので記事にしておきます。

環境

$ flutter doctor
[√] Flutter (Channel stable, 3.29.1, on Microsoft Windows [Version 10.0.26100.3775], locale ja-JP)
[√] Windows Version (11 Pro 64-bit, 24H2, 2009)
[√] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
[X] Chrome - develop for the web (Cannot find Chrome executable at .\Google\Chrome\Application\chrome.exe)
    ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable.
[√] Visual Studio - develop Windows apps (Visual Studio Community 2022 17.13.3)
[√] Android Studio (version 2024.3)
[√] VS Code (version 1.99.3)
[√] Connected device (2 available)
[√] Network resources

執筆時点ではChromeは入れてないです(常用ブラウザがChromeでないことと、Webアプリは開発スコープに含めてないこと以外に特段の理由はありません)

pubspec.yaml(抜粋)
  flutter_riverpod: ^2.6.1
  riverpod_annotation: ^2.6.1

dev_dependencies:
  build_runner: ^2.4.13
  test: ^1.25.7
  riverpod_generator: ^2.6.4

とりあえず (Async)NotifierProvider を overrideWith

なかなか情報が見つけられず、正しい書き方を見つけるまで四苦八苦しましたが
英語の情報も当たってみたら下記に辿り着いたのでこれに即してやってみたら上手くいきました。

コード例 test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'test.g.dart';

// プロダクトの(本物の)Provider
@riverpod
class TheNumber extends _$TheNumber {
  @override
  int build() => 0;

  void set(int num) => state = num;
}

// 上記プロバイダを参照しているTheNumberServiceクラス(非ウィジェット)・
// TheNumberWidgetクラス があるものとします(実装省略)

// theNumberProviderを上書きする用のクラス(以下「上書きクラス」)
@riverpod
class TheNumberStub extends _$TheNumberStub implements TheNumber {
  @override
  int build() => 42;  // 常時固定の値とする

  // implements TheNumber しているので TheNumber クラスの全メンバ・全メソッドの @override が必須
  @override
  void set(int num) => ();
}

// テストコード
void main() {
  test('TheNumberService クラスのテスト', () {
    final container = ProviderContainer(overrides: [
      repositoryProvider.overrideWith(() => TheNumberStub()),
    ]);

    final service = TheNumberService();
    // service についての各種検証:expect(...)

    container.dispose();
  });

  testWidgets('TheNumberWidget のウィジェットテスト', (tester) async {
    await tester.pumpWidget(ProviderScope(overrides: [
      repositoryProvider.overrideWith(() => TheNumberStub()),
    ], child: const TheNumberWidget()));

    // Widget についての各種検証:expect(find. ... , findsNothing/findsOneWidget/...)
  });
}

上書きクラスは下記のように元の TheNumber クラスを直接 extends しても動きました。
この場合 build はやはり @override 必須ですが、それ以外で挙動を特に変える必要がないメソッドやメンバは @override しなくても大丈夫です。

  @riverpod
- class TheNumberStub extends _$TheNumberStub implements TheNumber {
+ class TheNumberStub extends TheNumber {
    @override
    int build() => 42;
-
-   @override
-   void set(int num) => ();
  }

AsyncNotifierProvider でも、いずれの方法で上書きクラスを実装しても NotifierProvider の場合と全く同様に動かせました。

(少々逸脱)モック? スタブ? フェイク?

ここまで overrideWith で上書きする用のクラスを敢えて本記事独自の呼称で濁した書き方をしてきましたが、言わずもがな上書きを行う動機は十中八九テストコードで本当のオブジェクトの代わりをさせるためかと思います。
要はモックのことね、と思った方は本節も一読した方がいいかもしれません。スタブ(ないしテストダブル)のことね、と思った方は本節には恐らくご存知のことしか書いてませんので読み飛ばしてください。

その「本当のオブジェクトの代わり」をするもののことを一概に「モック」と言いがち1ですが、実際には「モック」というのはそのうちのある種のもののみを指しているに過ぎず、総称としては「テストダブル」と呼ぶのが正しいそうです。
では具体的にどうだったら「モック」なのか、「モック」ではないテストダブル(スタブフェイクなど)はどうだったらどれなのか――は本旨から外れるので詳細は割愛します(解説記事もちょっと探せば多数見つかります)が、数多の記事(勿論全ては読んでませんが)中で個人的に分かりやすかったものを下記に紹介しておきます。

で、これを踏まえた上でここまで「上書きクラス」と呼んでいたものは正しくは何なのか、という話ですが
少なくともテスト対象クラス・ウィジェットが上書きクラスをどう呼び出したかの検証2は不可能なので「モック」ではありません。
Generator での生成により Provider の機能を一通り持っているという意味では「フェイク」のような側面もなくはないかもですが、
役割としては state の値を固定しテスト対象に間接入力を与えるだけなので「スタブ」であると考えられます3

モックで overrideWith

(Async)NotifierProvider を(手製の)スタブoverrideWith するなら以上で問題ないですが、verify も行うために(本当の意味での)モック で行うにはどうすればよいのでしょうか。
verify の必要もあるなら Mockito 等で自動生成したモッククラスで overrideWith したい、となるのが人情(??)ですが、残念ながらそれは不可能なようです。

双方とも当初 Mockito で生成したモッククラスでの overrideWith を試みたものの上手くいかず4、モッククラスも(半)手製で作ることでどうにか達成できた様子。
本記事執筆時はスタブの挙動で十分だったのでこちらは実際に試しておらず、そのためサンプルコードも書いていませんのでモック化まで必要な方はこれらをご参考に願います。

公式見解(そもそも論)

(Async)NotifierProvider のモック化はそもそも非推奨で、代わりにその依存先をモック化すべきと Riverpod のドキュメントには書いてありました:

でもそうは言っても、モック化まではともかくとしてもスタブ化、即ち (Async)NotifierProvider のメソッドに決まった値を返させたいときはありますよねえ5
そういう場面にぶつかった際 本記事がお役に立てば、ということで。

その他参考情報

  1. そして筆者自身も本記事を書き始めた時点ではそうだった(今回初めてきちんと勉強・理解した)ので当初「モック」で書き通しかけた

  2. Mockito 等でいうところの verify

  3. そのため参考記事を基に書いたコードサンプルでも「Fake~」は「~Stub」にした

  4. リンク先にあるようなコンパイルエラーも出るし、(Async)NotifierProvider から自動生成したモックに対し実際に when で挙動を定義しようとすると state がスコープ外で操作できないことからも一筋縄に行かないのが分かる

  5. DIをコンストラクタ引数で実現していればスタブで overrideWith する必要もないという見解もあるかもしれないが、Riverpod を最大限活用するならDIも overrideWith で実現したいかなと

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?