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)
}
}
// ...
}
*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.