👩‍🔧

Firebaseのテストコード書いてみる

2022/12/28に公開

FireStoreのmockがあった!

最近テストコードを書く勉強をしてるのですが、HTTPを使ったときは、mockを使ってたので、firebaseでもあるんじゃないかと思っていたら、FlutterFireを見ていたときに見つけました!

今回はこちらのpackageを使用
https://pub.dev/packages/fake_cloud_firestore
Timestampを使いたかったので、cloud firestoreのpackageをインストールしました。
これがないと、Timestampが使えない!
テストをするときは、専用のpackageをinstallする必要があったので、こちらを追加してpub getします。

flutter_driver:
    sdk: flutter

今回使用したpabspec.yamlの設定

pabspec.yaml
name: fake_cloud_test
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=2.18.0 <3.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  fake_cloud_firestore: ^2.1.0
  firebase_core: ^2.4.0
  cloud_firestore: ^4.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_driver: # firestoreのテスト用のpackageを追加
    sdk: flutter   # firestoreのテスト用のpackageを追加

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

main.dartには、Githubのリポジトリで公開されていたコードをそのまま使いました。

main.dart
// Copyright 2017, the Chromium project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart';

Future<void> main() async {
  enableFlutterDriverExtension();
  WidgetsFlutterBinding.ensureInitialized();
  final app = await Firebase.initializeApp(
    name: 'test',
    options: const FirebaseOptions(
      appId: '1:79601577497:ios:5f2bcc6ba8cecddd',
      messagingSenderId: '79601577497',
      apiKey: 'AIzaSyArgmRGfB5kiQT6CunAOmKRVKEsxKmy6YI-G72PVU',
      projectId: 'flutter-firestore',
    ),
  );
  final firestore = FirebaseFirestore.instanceFor(app: app);

  runApp(MaterialApp(
      title: 'Firestore Example', home: MyHomePage(firestore: firestore)));
}

class MessageList extends StatelessWidget {
  MessageList({required this.firestore});

  final FirebaseFirestore firestore;

  
  Widget build(BuildContext context) {
    return StreamBuilder<QuerySnapshot>(
      stream: firestore.collection('messages').snapshots(),
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        final data = snapshot.data;
        if (!snapshot.hasData || data == null) return const Text('Loading...');
        final messageCount = data.docs.length;
        return ListView.builder(
          itemCount: messageCount,
          itemBuilder: (_, int index) {
            final document = data.docs[index];
            final dynamic message = document.get('message');
            return ListTile(
              title: Text(
                message != null ? message.toString() : '<No message retrieved>',
              ),
              subtitle: Text('Message ${index + 1} of $messageCount'),
            );
          },
        );
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  MyHomePage({required this.firestore});

  final FirebaseFirestore firestore;

  CollectionReference get messages => firestore.collection('messages');

  Future<void> _addMessage() async {
    await messages.add(<String, dynamic>{
      'message': 'Hello world!',
      'created_at': FieldValue.serverTimestamp(),
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Firestore Example'),
      ),
      body: MessageList(firestore: firestore),
      floatingActionButton: FloatingActionButton(
        onPressed: _addMessage,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

テストコードは、testディレクトリの中でしか実行できないので、こちらに作成します。

testディレクリのファイル

test
├── fake_add.dart
├── fake_delete_test.dart
├── fake_update_test.dart
└── widget_test.dart

main関数左の矢印のボタンを押すと、テストコードを実行できます。

追加のテストをするファイル

test/fake_add.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';

void main() async {
  final instance = FakeFirebaseFirestore();
  await instance.collection('users').add({
    'username': 'Bob',
    'age': 24,
    'updatedAt': Timestamp.fromDate(DateTime.now())
  });
  final snapshot = await instance.collection('users').get();
  print(snapshot.docs.length); // 1
  print(snapshot.docs.first.get('username')); // Bob
  print(snapshot.docs.first.get('age')); // 24
  print(snapshot.docs.first
      .get('updatedAt')); // Timestamp(seconds=1672208471, nanoseconds=45978000)
  print(instance.dump());
}

実行結果

1
Bob
24
Timestamp(seconds=1672208791, nanoseconds=181349000)
{
  "users": {
    "T5s02f3LjU5UdEQOcuBz": {
      "username": "Bob",
      "age": 24,
      "updatedAt": "2022-12-28T15:26:31.181349"
    }
  }
}

更新をするテスト

test/fake_update.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';

void main() async {
  final instance = FakeFirebaseFirestore();
  await instance.collection('users').doc('id01').set({
    'username': 'Jim',
    'age': 30,
    'updatedAt': Timestamp.fromDate(DateTime.now())
  });
  final snapshot = await instance.collection('users').get();
  print(snapshot.docs.length); // 1
  print(snapshot.docs.first.get('username')); // Jim
  print(snapshot.docs.first.get('age')); // 30
  print(snapshot.docs.first.get(
      'updatedAt')); // Timestamp(seconds=1672206486, nanoseconds=674525000)
  print(instance.dump());
}

実行結果

1
Jim
30
Timestamp(seconds=1672209102, nanoseconds=172089000)
{
  "users": {
    "id01": {
      "username": "Jim",
      "age": 30,
      "updatedAt": "2022-12-28T15:31:42.172089"
    }
  }
}
test/fake_delete.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';

void main() async {
  final instance = FakeFirebaseFirestore();
  await instance.collection('users').doc('id01').delete();
  final snapshot = await instance.collection('users').get();
  print(snapshot.docs.length); // 0
  print(instance.dump());
}

実行結果
何もないので、0ですね。

0
{
  "users": {}
}

最後に

今回は、mockがあるのを見つけてテストを試してみました。
Flutterのテストコードに詳しい方がいたらコメント頂きたいです。
Firebaseでテストをするならどのような方法がベストなのか今探しているところです。

Discussion