🙄

Flutter(Dart)のテスト技法

に公開

Flutter のアプリ開発では、アーキテクチャとして MVVM が採用されるケースがしばしばあります。
その中でデータ層やビジネスロジックの実装として以下の技術を利用されている。

  • StateNotifier
  • Stream
  • Future

これらのテスト技法についてまとめました。

MVVM について

アプリの扱うデータを持つためのローカル・リモートのデータソースがあり、
そのローカル・リモートを抽象化するためのリポジトリまでをデータ層と呼びます。
UI の状態ホルダーとなる ViewModel がリポジトリを介してデータを取得し、
そのデータを整形し UI 層に公開します。

mvvm
出典:https://developer.android.com/jetpack/guide?hl=ja

非同期処理をテストする

API リクエストや shared preference 等、リモート・ローカルのデータソースからデータを取得するような、非同期で行われる処理についてテストを書いていきます。

準備

fake_asyncをインストールしてください。

flutter pub add --dev fake_async

delay を入れて偽装したテスト

API を通してリモートのデータソースから取得するテストを考えていきます。データソースで API リクエストする処理はテスト時は偽装し、delay を入れてテストすることがしばしばあるかと思います。
fake_async を使うとテストの現実の実行時間は変えずに、偽装的な時間で delay を入れることができます。

次のメソッドfetchDataは排他処理がされている。ここで排他処理についてのテストを書いていきます。

class FooRepository {
....
  FooRepository({required DataSource dataSource})
      : _dataSource = dataSource;
  final DataSource _dataSource;

  // 排他処理用のフラグ
  bool _isFetching = false;

  // このメソッドのテストを書いていきます
  Future<void> fetchData(final String url) async {
    if (!_isFetching) {
      _isFetching = true;
      _dataSource.fetchData(url);
      _isFetching = false;
    }
  }
}


// データソースは次のようにdelayで偽装
class SpyRemoteDetaSource implements DataSource {
....

  // fetchData()の呼び出し回数を記録
  int callCount = 0;

  Future<void> fetchData(final String url) async {
    callCount++;
    // データ取得処理をdelayで偽装
    await Future.delayed(const Duration(milliseconds: 500));
  }
}

テストコードを見ていきます。

test("fetchDataの排他制御", () {
  // テストのブロックをfakeAsyncで囲みます (1)
  fakeAsync((fakeTime) {
    final spyDataSource = SpyRemoteDetaSource();
    final repository = FooRepository(dataSource: spyDataSource);
    repository.fetchData("url");
    repository.fetchData("url");
    expect(spyDataSource.callCount, 1);

    // 偽装時間を進めます (2)
    fakeTime.elapse(const Duration(milliseconds: 500));
    repository.fetchData("url");
    expect(spyDataSource.callCount, 2);
  });
});
  1. テストのブロックを fakeAsync で囲みます。
  2. FakeAsync.elapse()で偽装時間を進めます。

Stream をテストする

次のように Stream を使って上位の層にデータを公開し、上位層で listen する実装パターンについてのテストを考えていきます。

// データを流し込むストリーム
final _settingStreamController = StreamController<String>.broadcast();

// 外部に公開する
Stream getSettingStream() {
  return _settingStreamController.stream;
}

// 設定を保存する
void saveSetting(final String newValue) {
  _settingStreamController.sink.add(newValue);
}

テストコードを見ていきます。

test("streamに更新後の値が流れてくる", () async {
  final repository = SettingsRepository();
  // Streamに流れてくる順に値をmatcherに指定する
  expectLater(repository.getSettingStream(), emitsInOrder(["設定1", "設定2"]));
  await repository.saveSetting("設定1");
  await repository.saveSetting("設定2");
});

matcher として emitsInOrder を使い、Stream に流れてくる値を順に列挙します。

StateNotifier をテストする

Riverpod を採用しているプロジェクトでは、StateNotifier を ViewModel として利用していることがあります。
ここでは StateNotifier のテスト方法を記載します。

データ読み込み時にローディングインジケーターを表示するような処理にてロード中の状態を更新します。
このような処理のテストコードを見ていきます。

test("ロード中の状態更新", () {
  final viewModel = HomeViewModel();
  viewModel.load();
  // debugStateにアクセスし状態を参照する
  expect(viewModel.debugState, const HomeState(isLoading: true));
});

まとめ

  • 非同期処理を delay で偽装するときは、fake_async を使用することで偽装的な時間を遅延し、現実の実行時間には影響を与えずにテストできます。
  • Stream のテストは、expectLater の matcher に emitsInOrder に流れてくる値を順番に指定します。
  • StateNotifier のテストは、debugState で状態を参照します。
GitHubで編集を提案

Discussion