skip to Main Content

I am trying to use the Firebase Auth and Firestore emulator for testing, but my real Firebase app for development. I have Hilt for dependency injection. In my test module, I set useEmulator but in my development module, I just use the Firebase singletons. It turns out development is still using the emulator because the singleton is shared between tests and development. How do I disconnect from the emulator in the development module?

Development module:

@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {
    @Singleton
    @Provides
    fun provideAuth(): FirebaseAuth = Firebase.auth

    @Singleton
    @Provides
    fun provideDb(): FirebaseFirestore = Firebase.firestore
}

Test module:

@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [FirebaseModule::class])
object FakeFirebaseModule {
    private val TAG = FakeFirebaseModule::class.simpleName

    @Singleton
    @Provides
    fun provideAuth(): FirebaseAuth = Firebase.auth.apply {
        try {
            useEmulator("10.0.2.2", 9099)
        } catch (e: IllegalStateException) {
            Log.e(TAG, "User emulator failed", e)
        }
    }

    @Singleton
    @Provides
    fun provideDb(): FirebaseFirestore = Firebase.firestore.apply {
        try {
            useEmulator("10.0.2.2", 8080)
        } catch (e: IllegalStateException) {
            Log.e(TAG, "User emulator failed", e)
        }
        firestoreSettings = FirebaseFirestoreSettings.Builder().setPersistenceEnabled(false).build()
    }
}

Test:

@UninstallModules(FirebaseModule::class)
@HiltAndroidTest
@MediumTest
class ExampleTest {
    private val hiltRule = HiltAndroidRule(this)

    private val composeTestRule = createAndroidComposeRule<MainActivity>()

    @get:Rule
    val rule: TestRule = RuleChain.outerRule(hiltRule).around(composeTestRule)

    @Inject
    lateinit var auth: FirebaseAuth

    @Inject
    lateinit var db: FirebaseFirestore

    @Before
    fun setUp() {
        hiltRule.inject()
        auth.createUserWithEmailAndPassword(TestData.UserEmail1, TestData.UserPassword1)
            .addOnFailureListener {
                auth.signInWithEmailAndPassword(TestData.UserEmail1, TestData.UserPassword1)
            }
    }

    // ...
}

2

Answers


  1. Chosen as BEST ANSWER

    *sigh* I figured out what was wrong and I was looking in completely the wrong place. I thought my development code was using test data, but actually it was using stale development data. I had this in my code:

    inline fun <reified T : Any> Query.getObjectsFlow() = callbackFlow {
        val subscription = addSnapshotListener { value, error ->
            when {
                error != null -> cancel("Get snapshot failed", error)
                value != null -> trySend(value.toObjects<T>())
            }
        }
    
        awaitClose { subscription.remove() }
    }
    

    used like this:

    // Repository
    
    val feed = db.collection(FEED_COLLECTION)
                .getObjectsFlow<FeedItemDocument>()
                .map { docs ->
                    val feedItems = docs.map { FeedItemState.fromDocument(it) }
                    if (feedItems.isEmpty()) {
                        LoadState.Empty()
                    } else {
                        LoadState.Data(Feed(feedItems))
                    }
                }
                .catch {
                    Log.e(TAG, "Error querying server", it)
                    emit(LoadState.Failure(it))
                }
    
    // FeedItemState
    
    data class FeedItemState(
        val id: String = "",
        val createdAt: Instant = Instant.now(),
        val text: String = "",
    ) {
        companion object {
            fun fromDocument(doc: FeedItemDocument): FeedItemState {
                val id = doc.id!!
                val createdAt = doc.createdAt?.toDate()?.toInstant()!!
                val text = doc.text!!
                return FeedItemState(type, id, createdAt, text)
            }
        }
    }
    

    My repository would emit a load failure value if any of the document fields were null. However, the snapshot listener emits every single query object including old ones. Which means when I would add fields to the database, the first object emitted would not have the new field, thus throwing an NPE.

    I had to change the repository to accept and ignore null fields. So changing docs.map { FeedItemState.fromDocument(it) } to mapNotNull and allowing the fromDocument method to return null when the fields were null instead of using !!.

    Lesson learned: In Kotlin if you're using !!, be very suspicious of the code. It can often be replaced with something that doesn't throw an NPE.


  2. There doesn’t appear to be a disconnect feature for the emulator (Firestore) and the fields are private when connecting. See this link.

    In that case, removing the @Singleton to get different Firestore instances, or scoping the singleton to the different (test vs prod) environments will keep the production Firestore from connecting to the emulator.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search