Flutterでfirebaseのauthとfirestoreを取り扱うriverpodを使ったunit testを構築する

こんにちは。[virapture株式会社](https://virapture.com)の[もぐめっと](https://mogmet.com)です。

![mogmet.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/d69351a5-7edb-01e9-2ee0-27212e0a8ce8.jpeg)
最近スキー場で[暁のメンバー](https://ja.wikipedia.org/wiki/%E6%9A%81_(NARUTO))に遭遇しました。思わず忍術を唱えてしまいました。

本日はテスト駆動開発大好きなもぐめっとがflutter X firestore X riverpodを使ったunit testの方法を模索してたどり着いた方法を紹介いたします。

結論から言うと、**emulatorは使えない**ので代用ライブラリを使って構築します！！

今回のサンプルは下記に作ってありますのでよかったらご参考ください。

https://github.com/mogmet/flutter_firestore_riverpod_unittest

## 前知識

調べた過程を紹介します。
（構築方法だけ知りたい場合は次の章へスキップしてください）

いつもfirestoreを使ったunit testを書くときはemulatorを用いていました。

（unityやandroidは下記のように構築してました。）

https://qiita.com/mogmet/items/23071f5a64457f7319ab

https://qiita.com/mogmet/items/f4c480c690e479c0fd40

いつものノリでemulatorを使っていい感じにできるっしょと思って調べたら・・・

https://github.com/firebase/flutterfire/issues/3311

```
I'm saying that using Firebase Emulators for your integration tests will work. But the example you're using is not an integration test, it is a unit test. A unit test does not involve any native API calls (to android or iOS for example). You have to mock the calls to the native platform (i.e Firebase.initializeApp()).
```

integration testでないと動かなそうというのと、integration testは遅いとのことでemulatorを使ったunit testは微妙そうでした。
（issueではmockすれば動くみたいな感じで書かれてましたが残念ながら動かせませんでした）

## 構築方法

ということでどうしたらいいかを調べたところ**firebase_auth_mocks**と**fake_cloud_firestore**というライブラリを使うことで代用できそうでした。

https://pub.dev/packages/firebase_auth_mocks

https://pub.dev/packages/fake_cloud_firestore

[fake_cloud_firestoreの説明文](https://pub.dev/packages/fake_cloud_firestore#features)も見る感じ大体対応してて良さそうです。

ということで今回は[User](https://github.com/mogmet/flutter_firestore_riverpod_unittest/blob/main/lib/user.dart)を取り扱う[UserRepository](https://github.com/mogmet/flutter_firestore_riverpod_unittest/blob/main/lib/user_repository.dart)に対してテストを書いてみることにしました。

```dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_firestore_unittest/providers.dart';
import 'package:flutter_firestore_unittest/user.dart' as app; // firebase authのUserと被るので別名定義
import 'package:flutter_riverpod/flutter_riverpod.dart';

// riverpodで使うためabstractにしてます。
abstract class AUserRepository {
  // ユーザ取得
  Future<app.User?> fetchUser(String authId);

  // 匿名ユーザの作成
  Future<app.User> createUser();
}

class UserRepository implements AUserRepository {
  UserRepository(Reader reader)
      : _firestore = reader(firebaseFirestoreProvider),
        _auth = reader(firebaseAuthProvider);
  final FirebaseFirestore _firestore;
  final FirebaseAuth _auth;

  CollectionReference get _collection => _firestore.collection('users');

  DocumentReference _getUserReference(String authId) {
    return _collection.doc(authId);
  }

  @override
  Future<app.User?> fetchUser(String authId) async {
    final snapshot = await _getUserReference(authId).get();
    return app.User.fromSnapshot(snapshot);
  }

  @override
  Future<app.User> createUser() async {
    final authUser = await _auth.signInAnonymously();
    final authId = authUser.user?.uid ?? '';
    final user = app.User(authId: authId, username: 'ワンナイト人狼だいすキッズ');
    await _getUserReference(authId).set(user.toJson());
    return user;
  }
}
```

テストはriverpodと組み合わせるテストにしてますが、こんな感じで書けるようになりました。
```dart
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
import 'package:flutter_firestore_unittest/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

@Timeout(Duration(seconds: 2))
void main() {
  late ProviderContainer container;
  const uid = 'werewolf';
  setUp(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    final mockUser = MockUser(isAnonymous: true, uid: uid); // FirebaseAuthで返却予定のユーザ
    // authとfirestoreをそれぞれmock化しちゃいます。
    container = ProviderContainer(
      overrides: [
        firebaseAuthProvider.overrideWithProvider(
          Provider((ref) => MockFirebaseAuth(mockUser: mockUser)),
        ),
        firebaseFirestoreProvider.overrideWithProvider(
          Provider((ref) => FakeFirebaseFirestore()),
        ),
      ],
    );
  });
  test('ユーザが作成できるか', () async {
    await container.read(userRepositoryProvider).createUser();
    final user = await container.read(userRepositoryProvider).fetchUser(uid);
    expect(user!.authId, uid);
    expect(user.username, 'ワンナイト人狼だいすキッズ');
  });
}

```

まるでemulatorを使ってるかのように振る舞ってくれました！！これはありがたい！！

一応サンプルを再掲しておきます。

https://github.com/mogmet/flutter_firestore_riverpod_unittest


## 注意事項

firebase_auth_mocksに関しての注意事項になります。

### SNSログインはGoogleだけ

Google signinは別途[google_sign_in_mocks](https://pub.dev/packages/google_sign_in_mocks)を使って実施できるみたいですが、twitter/apple/facebookといったモックログインには対応してないみたいなので、どれのsignin方法を選んでもロジック的になるべく同じロジックで処理が通るようにコードを書く必要がありそうです。
ロジックさえ合わせられればgoogle signinのunit testだけでなんとかなりそうと思っています。

### Emailログインでの登録はmockUserが上書きされる

createUserWithEmailAndPasswordのメソッドでログインしようとするとuidがmock_uidとして上書きされちゃいますので要注意です。
（多分バグ）

該当コード↓

https://github.com/atn832/firebase_auth_mocks/blob/master/lib/src/firebase_auth_mocks_base.dart#L73

## まとめ

**fake_cloud_firestore**と**firebase_auth_mocks**を使うことで、flutterでも快適なunit test生活を送れます！！！！！


最後に、[ワンナイト人狼オンライン](https://online.1nite-jinro.com/link/blog)というゲームを作ってます！よかったら遊んでね！

https://online.1nite-jinro.com/link/blog

他にも[Camecon](https://camecon.me/)や[Offcha](https://offcha.com/)といったサービスも作ってるのでよかったら使ってね！

https://camecon.me/

https://offcha.com/

また、チームビルディングや技術顧問、Firebaseの設計やアドバイスといったお話も受け付けてますので御用の方は弊社までお問い合わせください。

https://virapture.com/