🧪

【Flutter】riverpod genarator を使ったProviderのユニットテスト

2024/06/12に公開

🔥 導入

Flutterでriverpod generatorを使ったProviderのユニットテストコードを通して、基本的なテストコードの書き方をまとめておきます。
Poke Matchという勉強用アプリの中で使用しているProviderを例にします。
しょうもないアプリですが、気になる方は見てみてください。
ポケモンとTinder風UI/UXでマッチングアプリ体験できます。
https://pokemon-matching.web.app/
※パッケージの導入方法などは割愛します🙇‍♂️

🎯 テスト対象ファイル

以下は、1~1,000までの数字をランダムな順番で持つList<int>型を提供するProviderです。

id_list_provider.dart
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'id_list_provider.g.dart';

(keepAlive: true)
class IdList extends _$IdList {
  
  List<int> build() {
    return List.generate(1000, (index) => index + 1)..shuffle();
  }

  void remove() {
    final newList = List<int>.from(state);
    newList.removeAt(0);
    state = newList;
  }

  /// test用にStateを返す
  List<int> debugState() {
    return state;
  }
}

そもそも何でテストコード書くの?って人🙋

本編とは関係ないのでスキップでも👍🏻

はい、僕もそうでした。
動いてればそれで良くね?と(エンジニアとしてクソなのかもしれないwww)
テストコード書いてる暇あったらどんどん開発を進めた方がいいじゃん!と
結論...
個人開発では多少のバグがあっても問題ないと思うので、どんどん機能開発しちゃってもいいんじゃないかなと思います。
テストコードを書くのも時間かかりますし、新しい機能開発してる方がワクワクするし。
ただし...
規模の大きいアプリや、実際にユーザーや導入企業にお金を払ってもらうようなアプリの場合、多少のバグは目をつぶろう、とはなりません。
じゃあテストを全て手動で目視で行うのか?というとかなり大変だし、テストする人によって精度にばらつきが出ます。
アプリの品質を担保するためにも、テストコード書いてテストするの大事!!

✅ テストコード

解説は後回しで、とりあえず完成系。
以下のテストコードの中に、2つのテストケースが書かれています。

id_list_provider_test.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:poke_match/core/state/id_list_provider.dart';

void main() {
  // 複数のテストをまとめる
  group('IdList Tests', () {
    late ProviderContainer container;

    // 各テストの前に実行
    setUp(() {
      container = ProviderContainer();
    });

    // 各テストの後に実行
    tearDown(() {
      container.dispose();
    });

    // 1個目のテスト
    test('build test', () {
      final notifier = container.read(idListProvider.notifier);

      notifier.build();

      // リストが1000要素を持っているか
      expect(notifier.debugState(), hasLength(1000));

      // リストに1が含まれるか
      expect(notifier.debugState(), contains(1));

      // リストに1,000が含まれるか
      expect(notifier.debugState(), contains(1000));

      // リストに0が含まれていないか
      expect(notifier.debugState(), isNot(contains(0)));

      // リストに1,001が含まれていないか
      expect(notifier.debugState(), isNot(contains(1001)));
    });

    // 2個目のテスト
    test('remove method test', () {
      final notifier = container.read(idListProvider.notifier);

      notifier.build();

      final first = notifier.debugState().first;

      // リストが1000要素を持っているか
      expect(notifier.debugState(), hasLength(1000));

      notifier.remove();

      // リストが999要素になっているか
      expect(notifier.debugState(), hasLength(999));

      // 最初の要素が削除されたか
      expect(notifier.debugState(), isNot(contains(first)));
    });
  });
}

1.テストコードってどこに書く?

そもそもこのテストコードってFlutterプロジェクトのどこに書くのか?
testディレクトリの中にlib同様のディレクトリ構成でファイルを作成します。
以下のような感じ。

libディレクトリ
lib
├── core
│   ├── constants
│   └── state
│       ├── list_id_provider.dart // テスト対象ファイル
│       └── foo.dart
│
├── main.dart
testディレクトリ
test
├── core
│   ├── constants
│   └── state
│       ├── list_id_provider_test.dart // テストファイル
│       └── foo_test.dart
│
├── main.dart

※ファイル名は必ず {libディレクトリ内でつけているファイル名}_test.dart です。

2.テストコードの基本構文

基本的なテストコードは以下のようになります。
main関数の中にtest関数を書いて、2つ目の引数の中にテストコードを書くだけ!
なんて簡単なんでしょう!!

foo_test.dart
void main() {
  test('ここにこのテストの説明を書くよ', () {
    // テストコードをを記述
  });
}

もう少し詳しく知りたい方は、下記の公式チュートリアルを見てください。
https://docs.flutter.dev/cookbook/testing/unit/introduction

3.テストコードの解説

基本的な書き方はわかったところで実際のテストコードの解説。
実際のテストコードには、

  • group()
  • setUp()
  • tearDown()
    のような、オプション関数を利用しています。
    何を行う関数なのかはコメントアウトしていますので詳細は割愛🙇
    詳細な説明やその他使えるオプション関数は以下の記事を参考にしてください!
    わかりやすいです!
    https://zenn.dev/ncdc/articles/flutter_unit_test

providerのユニットテストの準備

ユニットテストのセットアップについて見ていきます。以下がテストコードの冒頭部分です。

void main() {
  // 複数のテストをまとめる
  group('IdList Tests', () {
    late ProviderContainer container;

    // 各テストの前に実行
    setUp(() {
      container = ProviderContainer();
    });

    // 各テストの後に実行
    tearDown(() {
      container.dispose();
    });

late ProviderContainer container;
この部分では、ProviderContainerというRiverpodのコンテナを宣言しています。これは、テストの各ケースで使うプロバイダーを管理するためのものです。lateキーワードを使うことで、後から初期化することを明示しています。
ここで ProviderContainer はテストで使うrefみたいなものだと思ってOKかと思います。

setUptearDown
setUpとtearDownは、それぞれ各テストの前後に実行されるコードを指定するためのものです。

  • setUp
    各テストの前に必ず実行される初期化コードを記述します。ここでは、ProviderContainerを初期化しています。
  • tearDown
    各テストの後に必ず実行されるクリーンアップコードを記述します。ここでは、ProviderContainerを破棄しています。

これを行うことで、テストケースごとに新しい状態を用意し、テストが他のテストに影響を与えないようにするために、テストの都度初期化と破棄を行いテストの独立性を保てます!

テストケース1: build メソッドのテスト

テスト対象のbuildメソッド
list_id_provider.dart
part 'id_list_provider.g.dart';

(keepAlive: true)
class IdList extends _$IdList {
  
  List<int> build() {
    return List.generate(1000, (index) => index + 1)..shuffle();
  }
// 以下省略...
// 1個目のテスト
test('build test', () {
  final notifier = container.read(idListProvider.notifier);

  notifier.build();

  // リストが1000要素を持っているか
  expect(notifier.debugState(), hasLength(1000));

  // リストに1が含まれるか
  expect(notifier.debugState(), contains(1));

  // リストに1,000が含まれるか
  expect(notifier.debugState(), contains(1000));

  // リストに0が含まれていないか
  expect(notifier.debugState(), isNot(contains(0)));

  // リストに1,001が含まれていないか
  expect(notifier.debugState(), isNot(contains(1001)));
});

test()関数内には、なんらかの処理を書きますが、その後で
expect関数を使って、実行した処理が期待通りであるかを確認します。

以下のexpect関数を例に解説!

// リストが1000要素を持っているか
expect(notifier.debugState(), hasLength(1000));
  • 説明
    state(現在のリスト)の要素数が1000であることを確認します。
  • 使い方
    expect(actual, matcher)の形式で使います。hasLength(1000)はマッチャーで、リストの長さが1000であることを確認します。
  • matcherとは?
    テスト中に特定の条件を満たすかどうかを判定するために使用します。
    使用できるmatcher一覧は[公式参照](pub.dev/matcher library)。

build関数では1から1000までの数字をランダムな順番で持つListを生成するので、厳密に行うなら、以下のように全ての値が含まれているか?のテストでもいいのでしょうけど...
今回は境界値分析的な考え方でやっています。

forループver
for (int i = 1; i < 1001; i++) {
    except(notifier.debugState(), contains(i))
}

テストケース2: removeメソッドのテスト

テスト対象のremoveメソッド
list_id_provider.dart
void remove() {
    final newList = List<int>.from(state);
    newList.removeAt(0);
    state = newList;
}
// 以下省略...
// 2個目のテスト
test('remove method test', () {
  final notifier = container.read(idListProvider.notifier);

  notifier.build();

  final first = notifier.debugState().first;

  // リストが1000要素を持っているか
  expect(notifier.debugState(), hasLength(1000));

  notifier.remove();

  // リストが999要素になっているか
  expect(notifier.debugState(), hasLength(999));

  // 最初の要素が削除されたか
  expect(notifier.debugState(), isNot(contains(first)));
});

このテストでは、removeメソッドがリストの先頭要素を正しく削除するかどうかを確認しています。

removeメソッドを呼び出す前と後でリストの長さが変わることを確認します。
リストの最初の要素が削除されていることを確認します。

4.テストコードの実行

テストコードができたら、アプリプロジェクトのルートディレクトリ上で

実行コマンド
% flutter test

実行するだけ!!
すると...

出力結果
% flutter test
00:01 +2 ~2: All tests passed!

見事テストをパス✅しています!!
受験、資格試験、ユニットテスト。なんでも合格すると嬉しいですねw

📍 まとめ

flutterのユニットテストの基本的な書き方をまとめました。
そして、アプリの品質を高めるためにテストは大事!
FlutterでTDD(テスト駆動開発)もいつかしてみようかなぁ🤔

😏 おまけ

テストコードの解説に使った「Poke Match」ですがGitHubでコードも公開しています。
よかったら覗いてください。
https://github.com/go5go69/poke-match

ちなみにPokeAPIを使ってアプリを作るきっかけになったのは、前回の記事のおまけで、オーキド博士に言われたからですw

Discussion