Mocking and Testing Firestore Operations in Flutter Unit Tests | Part 1 (Documents and Collections)

Mocking and Testing Firestore Operations in Flutter Unit Tests | Part 1 (Documents and Collections)

Play this article

Introduction

This article examines Firestore operations with respect to Documents and Collections and demonstrates how to mock and test them.

The article was created using Flutter Channel stable, 2.8.1.

Prerequisites

Add the following packages to your pubspec.yaml file and run flutter pub get.

dependencies:
  ...
  cloud_firestore: ^3.1.4

dev_dependencies:
  ...
  test:
  fake_cloud_firestore: ^1.2.0

This adds cloud_firestore which is the Cloud Firestore package, test which is the package used for unit tests and fake_cloud_firestore which is the mock Firestore package.

Problem Statement

We have a class FirestoreService which performs read and write operations to Firestore and we want to write a set of unit tests for this class.

The class is shown below:

import 'package:cloud_firestore/cloud_firestore.dart';

class FirestoreService {
  const FirestoreService({required this.firestore});

  final FirebaseFirestore firestore;

  /// Collection Operations

  Future<DocumentReference<Map<String, dynamic>>> addToCollection(
      {required Map<String, dynamic> data,
      required String collectionPath}) async {
    return firestore.collection(collectionPath).add(data);
  }

  Future<QuerySnapshot<Map<String, dynamic>>> getFromCollection(
      {required String collectionPath}) {
    return firestore.collection(collectionPath).get();
  }

  Stream<QuerySnapshot<Map<String, dynamic>>> getSnapshotStreamFromCollection(
      {required String collectionPath}) {
    return firestore.collection(collectionPath).snapshots();
  }

  /// Document Operations

  Future<void> deleteDocumentFromCollection(
      {required String collectionPath, required String documentPath}) async {
    return firestore.collection(collectionPath).doc(documentPath).delete();
  }

  Future<DocumentSnapshot<Map<String, dynamic>>> getFromDocument(
      {required String collectionPath, required String documentPath}) {
    return firestore.collection(collectionPath).doc(documentPath).get();
  }

  Future<void> setDataOnDocument(
      {required Map<String, dynamic> data,
      required String collectionPath,
      required String documentPath}) {
    return firestore.collection(collectionPath).doc(documentPath).set(data);
  }

  Stream<DocumentSnapshot<Map<String, dynamic>>> getSnapshotStreamFromDocument(
      {required String collectionPath, required String documentPath}) {
    return firestore.collection(collectionPath).doc(documentPath).snapshots();
  }

  Future<void> updateDataOnDocument(
      {required Map<String, dynamic> data,
      required String collectionPath,
      required String documentPath}) {
    return firestore.collection(collectionPath).doc(documentPath).update(data);
  }
}

Testing Approach

Construction Injection

Since the class FirestoreService injects the Firestore object through its constructor, we can pass the FakeFirebaseFirestore object from the fake_cloud_firestore package and use that to test the methods in the class.

Mocking Strategy

The FakeFirebaseFirestore object works like the Firestore object and the read and write operations that can be used to retrieve and add data to the Firestore object can also be used to retrieve and add data to the FakeFirebaseFirestore object.

This helps us prepopulate the mock object right before we call our methods and helps confirm the expected state of the Firestore database.

Database State Set Up

  • Initial State

    The initial state of the FakeFirebaseFirestore object can be set up by calling the regular write operations on the CollectionReference and DocumentReference.

    For instance, we want the initial state of the database to have an object {answer: 42} written to a document lifeTheUniverseAndEverything in collection answers, we write the code below to set it up:

    final FakeFirebaseFirestore fakeFirebaseFirestore = FakeFirebaseFirestore();
    
    const String collectionPath = 'answers';
    const String documentPath = 'lifeTheUniverseAndEverything';
    
    const Map<String, dynamic> data = {'answer': 42};
    
    await fakeFirebaseFirestore
        .collection(collectionPath)
        .doc(documentPath)
        .set(data);
    
  • Final State

    The final state of the FakeFirebaseFirestore object can be gotten by calling the regular read operations on the CollectionReference and DocumentReference.

    Now, to get the final state of the FakeFirebaseFirestore object from above, we write the code below:

    final DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
        await fakeFirebaseFirestore
            .collection(collectionPath)
            .doc(documentPath)
            .get();
    final Map<String, dynamic> actualData = documentSnapshot.data()!;
    
    print(actualData); // {'answer': 42}
    
  • Assertion

    Once we have our data, our database's initial state set up and the final state retrieved, we can assert that the data in the database matches the expected data.

    Going by what we have above, we can write the assertion using the test package's expect method like below:

    expect(actualData, data);  /// Passes ✅
    

Testing The FirestoreService Class

We will test the FirestoreService class by following the approach described in the Testing Approach section above.

This section will be divided into three:

  • Set-up (where the objects needed for the tests are created)
  • Collection Operations Testing (where operations involving Collections are tested)
  • Document Operations Testing (where operations involving Documents are tested)

Set-up

Create a test file firestore_service_test.dart and add the following code to it:

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

void main() {
  group('FirestoreService', () {
    FakeFirebaseFirestore? fakeFirebaseFirestore;
    const Map<String, dynamic> data = {'data': '42'};

    setUp(() {
      fakeFirebaseFirestore = FakeFirebaseFirestore();
    });
  });
}

This creates the FakeFirebaseFirestore object and the data needed for every test we'll write for the FirestoreService class. All of the tests are put inside the "FirestoreService" group.

Collection Operations Testing

  • Map Equality

    This section will be testing Collection operations and this means we will be asserting that the collection contains the data. Typically, that is done by using the contains matcher.

    Returns a matcher that matches if the match argument contains the expected value.

    This is how an assertion using the contains matcher would be written:

    final List<int> intList = [1, 2];
    final int intData = 2;
    
    expect(intList, contains(intData));  /// Passes ✅
    

    This passes but when it comes to Lists of Maps, it's a different case.

    The assertion below fails:

    final List<Map<String, dynamic>> mapList = [
        {'data': 42}
    ];
    final mapData = {'data': 42};
    expect(mapList, contains(mapData)); /// Fails ❌
    

    This is because the contains matcher for Lists is implemented this way:

    return item.contains(_expected);
    

    which in turn uses the contains method on List which is implemented like below:

    bool contains(Object? element) {
     for (E e in this) {
       if (e == element) return true;
     }
     return false;
    }
    

    The code above checks that the e object is the same object as the element object. And the mapData object won't be the same as the one contained in the List. That explains why the test fails.

  • mapEquals

    In order to get around this, we can make use of the mapEquals function that does the following:

    Compares two maps for element-by-element equality.

    Returns true if the maps are both null, or if they are both non-null, they have the same lengths and they contain the same keys associated with the same values. Returns false otherwise.

    We can check if any of the element in the list is equal to the mapData object by using the mapEquals function like this:

     final List<Map<String, dynamic>> mapList = [
             {'data': 42}
     ];
     final mapData = {'data': 42};
     expect(mapList.any((element) => mapEquals(element, mapData)), true); /// Passes ✅
    

    This test passes because the equality check using mapEquals compares the two Map objects by confirming that the keys and values are the same.

  • Custom Matcher

    While using the check above would work for our tests, it's cleaner and more readable to follow the Flutter convention of using a Matcher to assert our test.

    We move the logic mapList.any((element) => mapEquals(element, mapData)) into the Matcher like this below:

     class MapListContains extends Matcher {
       final Map<dynamic, dynamic> _expected;
    
       const MapListContains(this._expected);
    
       @override
       Description describe(Description description) {
         return description.add('contains ').addDescriptionOf(_expected);
       }
    
       @override
       bool matches(dynamic item, Map matchState) {
         if (item is List<Map>) {
           return item.any((element) => mapEquals(element, _expected));
         }
         return false;
       }
     }
    

    The MapListContains class extends the Matcher abstract class and overrides the describe method and the matches method. The assertion logic is implemented in the matches method in the MapListContains class.

    So we can update the test to this below:

     final List<Map<String, dynamic>> mapList = [
             {'data': 42}
     ];
     final mapData = {'data': 42};
     expect(mapList, MapListContains(mapData));  /// Passes ✅
    

    And this test passes.

  • Set-up

    Now, we move to the actual test set-up.

    We first create a group "Collection Operations" to hold tests for collection operations.

    import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
    import 'package:test/test.dart';
    
    void main() {
      group('FirestoreService', () {
        FakeFirebaseFirestore? fakeFirebaseFirestore;
        const Map<String, dynamic> data = {'data': '42'};
    
        setUp(() {
           fakeFirebaseFirestore = FakeFirebaseFirestore();
        });
    
        group('Collection Operations', () {
    
        });
      });
     }
    

    We, can add each test inside of the group.

  • Test For Adding To A Collection.

     Future<DocumentReference<Map<String, dynamic>>> addToCollection(
        {required Map<String, dynamic> data,
         required String collectionPath}) async {
       return firestore.collection(collectionPath).add(data);
     }
    

    The addToCollection method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Adding the data to the collection via the service's addToCollection method.
    • Retrieving the actual data list in the collection via fakeFirebaseFirestore.
    • Asserting that the actual data list contains the added data using the MapListContains matcher.

    This is shown below:

     test('addToCollection adds data to given collection', () async {
         final FirestoreService firestoreService =
                FirestoreService(firestore: fakeFirebaseFirestore!);
         const String collectionPath = 'collectionPath';
    
         await firestoreService.addToCollection(
                data: data, collectionPath: collectionPath);
    
         final List<Map<String, dynamic>> actualDataList =
                (await fakeFirebaseFirestore!.collection('collectionPath').get())
                    .docs
                    .map((e) => e.data())
                    .toList();
    
         expect(actualDataList, const MapListContains(data));  /// Passes ✅
     });
    
  • Test For Getting Data From A Collection.

    Future<QuerySnapshot<Map<String, dynamic>>> getFromCollection() {
      return firestore.collection('collectionPath').get();
    }
    

    The getFromCollection method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Adding the data to the collection via the fakeFirebaseFirestore object.
    • Retrieving the actual data list in the collection via the service's getFromCollection method.
    • Asserting that the data list contains the added data using the MapListContains matcher.

    This is shown below:

     test('getFromCollection gets data from a given collection', () async {
        final FirestoreService firestoreService =
                FirestoreService(firestore: fakeFirebaseFirestore!);
        const String collectionPath = 'collectionPath';
    
        await fakeFirebaseFirestore!.collection(collectionPath).add(data);
    
        final List<Map<String, dynamic>> dataList = (await firestoreService
                    .getFromCollection(collectionPath: collectionPath))
                .docs
                .map((e) => e.data())
                .toList();
    
        expect(dataList, const MapListContains(data)); /// Passes ✅
     });
    
  • Test For Getting A Snapshot Stream From A Collection.

    Stream<QuerySnapshot<Map<String, dynamic>>> getSnapshotStreamFromCollection(
        {required String collectionPath}) {
      return firestore.collection(collectionPath).snapshots();
    }
    

    The getSnapshotStreamFromCollection method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service
    • Adding the data to the collection via the fakeFirebaseFirestore object.
    • Retrieving the expected snapshot stream from the fakeFirebaseFirestore object and the actual snapshot stream from the service's getSnapshotStreamFromCollection method.
    • Getting the data from the two snapshot streams and putting them into individual lists (the actual data list and the expected data list).
    • Asserting that the actual data list is equal to the expected data list.

    This is shown below:

     test( 'getSnapshotStreamFromCollection returns a stream of QuerySnaphot containing the data added', () async {
         final FirestoreService firestoreService =
                FirestoreService(firestore: fakeFirebaseFirestore!);
         const String collectionPath = 'collectionPath';
    
         final CollectionReference<Map<String, dynamic>> collectionReference =
                fakeFirebaseFirestore!.collection(collectionPath);
         await collectionReference.add(data);
    
         final Stream<QuerySnapshot<Map<String, dynamic>>>
                expectedSnapshotStream = collectionReference.snapshots();
    
         final actualSnapshotStream = firestoreService
                .getSnapshotStreamFromCollection(collectionPath: collectionPath);
    
         final QuerySnapshot<Map<String, dynamic>> expectedQuerySnapshot =
                await expectedSnapshotStream.first;
         final QuerySnapshot<Map<String, dynamic>> actualQuerySnapshot =
                await actualSnapshotStream.first;
    
         final List<Map<String, dynamic>> expectedDataList =
                expectedQuerySnapshot.docs.map((e) => e.data()).toList();
         final List<Map<String, dynamic>> actualDataList =
                actualQuerySnapshot.docs.map((e) => e.data()).toList();
    
         expect(actualDataList, expectedDataList); /// Passes ✅
     });
    

Document Operations Testing

  • Test For Deleting A Document From A Collection.

    Future<void> deleteDocumentFromCollection(
        {required String collectionPath, required String documentPath}) async {
      return firestore.collection(collectionPath).doc(documentPath).delete();
    }
    

    The deleteDocumentFromCollection method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Adding the data to the collection via the fakeFirebaseFirestore object.
    • Deleting the document at the collection via the service's deleteDocumentFromCollection method.
    • Getting the data at the document's path from the fakeFirebaseFirestore.
    • Asserting that the document does not exist.

    This is shown below:

    test('deleteDocumentFromCollection deletes a document from a given collection', () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
          const String collectionPath = 'collectionPath';
    
          final CollectionReference<Map<String, dynamic>> collectionReference =
              fakeFirebaseFirestore!.collection(collectionPath);
    
          final DocumentReference<Map<String, dynamic>> documentReference =
              await collectionReference.add(data);
    
          final String documentPath = documentReference.path;
    
          await firestoreService.deleteDocumentFromCollection(
              collectionPath: collectionPath, documentPath: documentPath);
    
          final DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
              await collectionReference.doc(documentPath).get();
    
          expect(documentSnapshot.exists, false); /// Passes ✅
        });
    
  • Test For Getting A Document's Data.

    Future<DocumentSnapshot<Map<String, dynamic>>> getFromDocument(
        {required String collectionPath, required String documentPath}) {
      return firestore.collection(collectionPath).doc(documentPath).get();
    }
    

    The getFromDocument method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Setting data to the document via the fakeFirebaseFirestore object.
    • Retrieving the actual data in the document via the service's getFromDocument method.
    • Asserting that the data retrieved via the service is equal to the data set via fakeFirebaseFirestore.

    This is shown below:

    test('getFromDocument gets data from a given document', () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
    
          const String collectionPath = 'collectionPath';
          const String documentPath = 'documentPath';
    
          final DocumentReference<Map<String, dynamic>> documentReference =
              fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);
    
          await documentReference.set(data);
    
          final DocumentSnapshot<Map<String, dynamic>> expectedDocumentSnapshot =
              await documentReference.get();
    
          final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
              await firestoreService.getFromDocument(
                  collectionPath: collectionPath, documentPath: documentPath);
    
          final Map<String, dynamic>? expectedData =
              expectedDocumentSnapshot.data();
          final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();
    
          expect(actualData, expectedData); /// Passes ✅
        });
    
  • Test For Setting Documents's Data.

    Future<void> setDataOnDocument(
        {required Map<String, dynamic> data,
        required String collectionPath,
        required String documentPath}) {
      return firestore.collection(collectionPath).doc(documentPath).set(data);
    }
    

    The setDataOnDocument method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Setting data to the document via the service's setDataOnDocument method.
    • Retrieving the actual data in the document via the fakeFirebaseFirestore object.
    • Asserting that the data set via the service is equal to the data retrieved via fakeFirebaseFirestore.

    This is shown below:

    test('setDataOnDocument sets data on a given document', () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
    
          const String collectionPath = 'collectionPath';
          const String documentPath = 'documentPath';
    
          await firestoreService.setDataOnDocument(
              data: data,
              collectionPath: collectionPath,
              documentPath: documentPath);
    
          final DocumentReference<Map<String, dynamic>> documentReference =
              fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);
    
          final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
              await documentReference.get();
          final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();
    
          const Map<String, dynamic> expectedData = data;
    
          expect(actualData, expectedData); /// Passes ✅
        });
    
  • Test For Getting A Snapshot Stream From A Document.

    Stream<DocumentSnapshot<Map<String, dynamic>>> getSnapshotStreamFromDocument(
        {required String collectionPath, required String documentPath}) {
      return firestore.collection(collectionPath).doc(documentPath).snapshots();
    }
    

    The getSnapshotStreamFromDocument method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Setting the data to the document via the fakeFirebaseFirestore object.
    • Retrieving the expected snapshot stream from the fakeFirebaseFirestore object and the actual snapshot stream from the service's getSnapshotStreamFromDocument method.
    • Getting the data from the two snapshot streams.
    • Asserting that the actual data via the service is equal to the expected data via fakeFirebaseFirestore.

    This is shown below:

    test(
            'getSnapshotStreamFromDocument returns a stream of DocumentSnapshot containing the data set',
            () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
    
          const String collectionPath = 'collectionPath';
          const String documentPath = 'documentPath';
    
          final DocumentReference<Map<String, dynamic>> documentReference =
              fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);
    
          await documentReference.set(data);
    
          final Stream<DocumentSnapshot<Map<String, dynamic>>>
              expectedSnapshotStream = documentReference.snapshots();
    
          final Stream<DocumentSnapshot<Map<String, dynamic>>>
              actualSnapshotStream =
              firestoreService.getSnapshotStreamFromDocument(
                  collectionPath: collectionPath, documentPath: documentPath);
    
          final DocumentSnapshot<Map<String, dynamic>> expectedDocumentSnapshot =
              await expectedSnapshotStream.first;
          final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
              await actualSnapshotStream.first;
    
          final Map<String, dynamic>? expectedData =
              expectedDocumentSnapshot.data();
          final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();
    
          expect(actualData, expectedData); /// Passes ✅
        });
    
  • Test For Updating A Document's Data.

    Future<void> updateDataOnDocument(
        {required Map<String, dynamic> data,
        required String collectionPath,
        required String documentPath}) {
      return firestore.collection(collectionPath).doc(documentPath).update(data);
    }
    

    The updateDataOnDocument method above can be tested by:

    • Injecting the mock Firestore object, fakeFirebaseFirestore, to the service.
    • Setting data to the document via the fakeFirebaseFirestore object.
    • Updating the data set to the document via the service's updateDataOnDocument method.
    • Retrieving the data in the document via fakeFirebaseStore.
    • Asserting that the updated data set via the service is equal to the expected data retrieved via fakeFirebaseFirestore.

    This is shown below:

     test('updateDataOnDocument updates a given document\'s data', () async {
       final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
    
       const String collectionPath = 'collectionPath';
       const String documentPath = 'documentPath';
    
       final DocumentReference<Map<String, dynamic>> documentReference =
              fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);
    
       await documentReference.set(data);
    
       final Map<String, dynamic> dataUpdate = {'data': '43'};
    
       await firestoreService.updateDataOnDocument(
              data: dataUpdate,
              collectionPath: collectionPath,
              documentPath: documentPath);
    
       final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
              await documentReference.get();
    
       final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();
    
       final Map<String, dynamic> expectedData = dataUpdate;
    
       expect(actualData, expectedData); /// Passes ✅
     });
    

Complete Test File

The content of the entire test file is shown below:

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fake_cloud_firestore/fake_cloud_firestore.dart';
import 'package:firestore_unit_test_flutter/firestore_service.dart';
import 'package:flutter/foundation.dart';
import 'package:test/test.dart';

void main() {
  group('FirestoreService', () {
    FakeFirebaseFirestore? fakeFirebaseFirestore;
    const Map<String, dynamic> data = {'data': '42'};

    setUp(() {
      fakeFirebaseFirestore = FakeFirebaseFirestore();
    });

    group(
      'Collection Operations',
      () {
        test('addToCollection adds data to given collection', () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
          const String collectionPath = 'collectionPath';

          await firestoreService.addToCollection(
              data: data, collectionPath: collectionPath);

          final List<Map<String, dynamic>> actualDataList =
              (await fakeFirebaseFirestore!.collection('collectionPath').get())
                  .docs
                  .map((e) => e.data())
                  .toList();

          expect(actualDataList, const MapListContains(data));
        });

        test('getFromCollection gets data from a given collection', () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
          const String collectionPath = 'collectionPath';

          await fakeFirebaseFirestore!.collection(collectionPath).add(data);

          final List<Map<String, dynamic>> dataList = (await firestoreService
                  .getFromCollection(collectionPath: collectionPath))
              .docs
              .map((e) => e.data())
              .toList();

          expect(dataList, const MapListContains(data));
        });

        test(
            'getSnapshotStreamFromCollection returns a stream of QuerySnaphot containing the data added',
            () async {
          final FirestoreService firestoreService =
              FirestoreService(firestore: fakeFirebaseFirestore!);
          const String collectionPath = 'collectionPath';

          final CollectionReference<Map<String, dynamic>> collectionReference =
              fakeFirebaseFirestore!.collection(collectionPath);
          await collectionReference.add(data);

          final Stream<QuerySnapshot<Map<String, dynamic>>>
              expectedSnapshotStream = collectionReference.snapshots();

          final actualSnapshotStream = firestoreService
              .getSnapshotStreamFromCollection(collectionPath: collectionPath);

          final QuerySnapshot<Map<String, dynamic>> expectedQuerySnapshot =
              await expectedSnapshotStream.first;
          final QuerySnapshot<Map<String, dynamic>> actualQuerySnapshot =
              await actualSnapshotStream.first;

          final List<Map<String, dynamic>> expectedDataList =
              expectedQuerySnapshot.docs.map((e) => e.data()).toList();
          final List<Map<String, dynamic>> actualDataList =
              actualQuerySnapshot.docs.map((e) => e.data()).toList();

          expect(actualDataList, expectedDataList);
        });
      },
    );

    group('Document Operations', () {
      test(
          'deleteDocumentFromCollection deletes a document from a given collection',
          () async {
        final FirestoreService firestoreService =
            FirestoreService(firestore: fakeFirebaseFirestore!);
        const String collectionPath = 'collectionPath';

        final CollectionReference<Map<String, dynamic>> collectionReference =
            fakeFirebaseFirestore!.collection(collectionPath);

        final DocumentReference<Map<String, dynamic>> documentReference =
            await collectionReference.add(data);

        final String documentPath = documentReference.path;

        await firestoreService.deleteDocumentFromCollection(
            collectionPath: collectionPath, documentPath: documentPath);

        final DocumentSnapshot<Map<String, dynamic>> documentSnapshot =
            await collectionReference.doc(documentPath).get();

        expect(documentSnapshot.exists, false);
      });

      test('getFromDocument gets data from a given document', () async {
        final FirestoreService firestoreService =
            FirestoreService(firestore: fakeFirebaseFirestore!);

        const String collectionPath = 'collectionPath';
        const String documentPath = 'documentPath';

        final DocumentReference<Map<String, dynamic>> documentReference =
            fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);

        await documentReference.set(data);

        final DocumentSnapshot<Map<String, dynamic>> expectedDocumentSnapshot =
            await documentReference.get();

        final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
            await firestoreService.getFromDocument(
                collectionPath: collectionPath, documentPath: documentPath);

        final Map<String, dynamic>? expectedData =
            expectedDocumentSnapshot.data();
        final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();

        expect(actualData, expectedData);
      });

      test('setDataOnDocument sets data on a given document', () async {
        final FirestoreService firestoreService =
            FirestoreService(firestore: fakeFirebaseFirestore!);

        const String collectionPath = 'collectionPath';
        const String documentPath = 'documentPath';

        await firestoreService.setDataOnDocument(
            data: data,
            collectionPath: collectionPath,
            documentPath: documentPath);

        final DocumentReference<Map<String, dynamic>> documentReference =
            fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);

        final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
            await documentReference.get();
        final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();

        const Map<String, dynamic> expectedData = data;

        expect(actualData, expectedData);
      });

      test(
          'getSnapshotStreamFromDocument returns a stream of DocumentSnapshot containing the data set',
          () async {
        final FirestoreService firestoreService =
            FirestoreService(firestore: fakeFirebaseFirestore!);

        const String collectionPath = 'collectionPath';
        const String documentPath = 'documentPath';

        final DocumentReference<Map<String, dynamic>> documentReference =
            fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);

        await documentReference.set(data);

        final Stream<DocumentSnapshot<Map<String, dynamic>>>
            expectedSnapshotStream = documentReference.snapshots();

        final Stream<DocumentSnapshot<Map<String, dynamic>>>
            actualSnapshotStream =
            firestoreService.getSnapshotStreamFromDocument(
                collectionPath: collectionPath, documentPath: documentPath);

        final DocumentSnapshot<Map<String, dynamic>> expectedDocumentSnapshot =
            await expectedSnapshotStream.first;
        final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
            await actualSnapshotStream.first;

        final Map<String, dynamic>? expectedData =
            expectedDocumentSnapshot.data();
        final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();

        expect(actualData, expectedData);
      });

      test('updateDataOnDocument updates a given document\'s data', () async {
        final FirestoreService firestoreService =
            FirestoreService(firestore: fakeFirebaseFirestore!);

        const String collectionPath = 'collectionPath';
        const String documentPath = 'documentPath';

        final DocumentReference<Map<String, dynamic>> documentReference =
            fakeFirebaseFirestore!.collection(collectionPath).doc(documentPath);

        await documentReference.set(data);

        final Map<String, dynamic> dataUpdate = {'data': '43'};

        await firestoreService.updateDataOnDocument(
            data: dataUpdate,
            collectionPath: collectionPath,
            documentPath: documentPath);

        final DocumentSnapshot<Map<String, dynamic>> actualDocumentSnapshot =
            await documentReference.get();

        final Map<String, dynamic>? actualData = actualDocumentSnapshot.data();

        final Map<String, dynamic> expectedData = dataUpdate;

        expect(actualData, expectedData);
      });
    });
  });
}

class MapListContains extends Matcher {
  final Map<dynamic, dynamic> _expected;

  const MapListContains(this._expected);

  @override
  Description describe(Description description) {
    return description.add('contains ').addDescriptionOf(_expected);
  }

  @override
  bool matches(dynamic item, Map matchState) {
    if (item is List<Map>) {
      return item.any((element) => mapEquals(element, _expected));
    }
    return false;
  }
}

Conclusion

This article demonstrates how to unit test Firebase Firestore operations involving Collections and Documents. It also shows how to use a custom Matcher in test assertions.

The complete code can be found on GitHub.