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
*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:
used like this:
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) }
tomapNotNull
and allowing thefromDocument
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.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.