Search code examples
androidfirebasekotlindagger-hiltfirebase-tools

How to disconnect from Firebase emulator using Android Hilt


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)
            }
    }

    // ...
}

Solution

  • *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.