Skip to content

Commit

Permalink
Adds unit test for SessionLifecycleClient. (#5475)
Browse files Browse the repository at this point in the history
  • Loading branch information
bryanatkinson authored Oct 26, 2023
1 parent 26990ac commit 7a1b9f7
Show file tree
Hide file tree
Showing 5 changed files with 391 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar {
)
}
.build(),
Component.builder(SessionLifecycleServiceBinder::class.java)
.name("sessions-service-binder")
.add(Dependency.required(firebaseApp))
.factory { container ->
SessionLifecycleServiceBinderImpl(container.get(firebaseApp))
}
.build(),
LibraryVersionComponent.create(LIBRARY_NAME, BuildConfig.VERSION_NAME),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions

import android.os.Looper
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.firebase.Firebase
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions
import com.google.firebase.concurrent.TestOnlyExecutors
import com.google.firebase.initialize
import com.google.firebase.sessions.api.FirebaseSessionsDependencies
import com.google.firebase.sessions.api.SessionSubscriber
import com.google.firebase.sessions.api.SessionSubscriber.SessionDetails
import com.google.firebase.sessions.testing.FakeFirebaseApp
import com.google.firebase.sessions.testing.FakeSessionLifecycleServiceBinder
import com.google.firebase.sessions.testing.FakeSessionSubscriber
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf

@OptIn(ExperimentalCoroutinesApi::class)
@MediumTest
@RunWith(RobolectricTestRunner::class)
internal class SessionLifecycleClientTest {

lateinit var fakeService: FakeSessionLifecycleServiceBinder

@Before
fun setUp() {
val firebaseApp =
Firebase.initialize(
ApplicationProvider.getApplicationContext(),
FirebaseOptions.Builder()
.setApplicationId(FakeFirebaseApp.MOCK_APP_ID)
.setApiKey(FakeFirebaseApp.MOCK_API_KEY)
.setProjectId(FakeFirebaseApp.MOCK_PROJECT_ID)
.build()
)
fakeService = firebaseApp.get(FakeSessionLifecycleServiceBinder::class.java)
}

@After
fun cleanUp() {
fakeService.serviceDisconnected()
FirebaseApp.clearInstancesForTest()
fakeService.clearForTest()
FirebaseSessionsDependencies.reset()
}

@Test
fun bindToService_registersCallbacks() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()

waitForMessages()
assertThat(fakeService.clientCallbacks).hasSize(1)
assertThat(fakeService.connectionCallbacks).hasSize(1)
}

@Test
fun onServiceConnected_sendsQueuedMessages() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()
client.foregrounded()
client.backgrounded()

fakeService.serviceConnected()

waitForMessages()
assertThat(fakeService.receivedMessageCodes)
.containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED)
}

@Test
fun onServiceConnected_sendsOnlyLatestMessages() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()
client.foregrounded()
client.backgrounded()
client.foregrounded()
client.backgrounded()
client.foregrounded()
client.backgrounded()

fakeService.serviceConnected()

waitForMessages()
assertThat(fakeService.receivedMessageCodes)
.containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED)
}

@Test
fun onServiceDisconnected_noMoreEventsSent() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()

fakeService.serviceConnected()
fakeService.serviceDisconnected()
client.foregrounded()
client.backgrounded()

waitForMessages()
assertThat(fakeService.receivedMessageCodes).isEmpty()
}

@Test
fun serviceReconnection_handlesNewMessages() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()

fakeService.serviceConnected()
fakeService.serviceDisconnected()
fakeService.serviceConnected()
client.foregrounded()
client.backgrounded()

waitForMessages()
assertThat(fakeService.receivedMessageCodes)
.containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED)
}

@Test
fun serviceReconnection_queuesOldMessages() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
client.bindToService()

fakeService.serviceConnected()
fakeService.serviceDisconnected()
client.foregrounded()
client.backgrounded()
fakeService.serviceConnected()

waitForMessages()
assertThat(fakeService.receivedMessageCodes)
.containsExactly(SessionLifecycleService.FOREGROUNDED, SessionLifecycleService.BACKGROUNDED)
}

@Test
fun doesNotSendLifecycleEventsWithoutSubscribers() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
client.bindToService()

fakeService.serviceConnected()
client.foregrounded()
client.backgrounded()

waitForMessages()
assertThat(fakeService.receivedMessageCodes).isEmpty()
}

@Test
fun doesNotSendLifecycleEventsWithoutEnabledSubscribers() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
val crashlyticsSubscriber = addSubscriber(false, SessionSubscriber.Name.CRASHLYTICS)
val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE)
client.bindToService()

fakeService.serviceConnected()
client.foregrounded()
client.backgrounded()

waitForMessages()
assertThat(fakeService.receivedMessageCodes).isEmpty()
}

@Test
fun sendsLifecycleEventsWhenAtLeastOneEnabledSubscriber() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE)
client.bindToService()

fakeService.serviceConnected()
client.foregrounded()
client.backgrounded()

waitForMessages()
assertThat(fakeService.receivedMessageCodes).hasSize(2)
}

@Test
fun handleSessionUpdate_noSubscribers() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
client.bindToService()

fakeService.serviceConnected()
fakeService.broadcastSession("123")

waitForMessages()
}

@Test
fun handleSessionUpdate_sendsToSubscribers() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
val perfSubscriber = addSubscriber(true, SessionSubscriber.Name.PERFORMANCE)
client.bindToService()

fakeService.serviceConnected()
fakeService.broadcastSession("123")

waitForMessages()
assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123"))
assertThat(perfSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123"))
}

@Test
fun handleSessionUpdate_sendsToAllSubscribersAsLongAsOneIsEnabled() =
runTest(UnconfinedTestDispatcher()) {
val client = SessionLifecycleClient(backgroundDispatcher() + coroutineContext)
val crashlyticsSubscriber = addSubscriber(true, SessionSubscriber.Name.CRASHLYTICS)
val perfSubscriber = addSubscriber(false, SessionSubscriber.Name.PERFORMANCE)
client.bindToService()

fakeService.serviceConnected()
fakeService.broadcastSession("123")

waitForMessages()
assertThat(crashlyticsSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123"))
assertThat(perfSubscriber.sessionChangedEvents).containsExactly(SessionDetails("123"))
}

private fun addSubscriber(
collectionEnabled: Boolean,
name: SessionSubscriber.Name
): FakeSessionSubscriber {
val fakeSubscriber = FakeSessionSubscriber(collectionEnabled, sessionSubscriberName = name)
FirebaseSessionsDependencies.addDependency(name)
FirebaseSessionsDependencies.register(fakeSubscriber)
return fakeSubscriber
}

private fun waitForMessages() {
shadowOf(Looper.getMainLooper()).idle()
}

private fun backgroundDispatcher() = TestOnlyExecutors.background().asCoroutineDispatcher()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.firebase.sessions.testing

import android.content.ComponentName
import android.content.ServiceConnection
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.os.Messenger
import com.google.firebase.sessions.SessionLifecycleService
import com.google.firebase.sessions.SessionLifecycleServiceBinder
import java.util.concurrent.LinkedBlockingQueue
import org.robolectric.Shadows.shadowOf

/**
* Fake implementation of the [SessionLifecycleServiceBinder] that allows for inspecting the
* callbacks and received messages of the service in unit tests.
*/
internal class FakeSessionLifecycleServiceBinder : SessionLifecycleServiceBinder {

val clientCallbacks = mutableListOf<Messenger>()
val connectionCallbacks = mutableListOf<ServiceConnection>()
val receivedMessageCodes = LinkedBlockingQueue<Int>()
var service = Messenger(FakeServiceHandler())

internal inner class FakeServiceHandler() : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
receivedMessageCodes.add(msg.what)
}
}

override fun bindToService(callback: Messenger, serviceConnection: ServiceConnection) {
clientCallbacks.add(callback)
connectionCallbacks.add(serviceConnection)
}

fun serviceConnected() {
connectionCallbacks.forEach { it.onServiceConnected(componentName, service.getBinder()) }
}

fun serviceDisconnected() {
connectionCallbacks.forEach { it.onServiceDisconnected(componentName) }
}

fun broadcastSession(sessionId: String) {
clientCallbacks.forEach { client ->
val msgData =
Bundle().also { it.putString(SessionLifecycleService.SESSION_UPDATE_EXTRA, sessionId) }
client.send(
Message.obtain(null, SessionLifecycleService.SESSION_UPDATED, 0, 0).also {
it.data = msgData
}
)
}
}

fun waitForAllMessages() {
shadowOf(Looper.getMainLooper()).idle()
}

fun clearForTest() {
clientCallbacks.clear()
connectionCallbacks.clear()
receivedMessageCodes.clear()
service = Messenger(FakeServiceHandler())
}

companion object {
val componentName =
ComponentName("com.google.firebase.sessions.testing", "FakeSessionLifecycleServiceBinder")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,10 @@ internal class FakeSessionSubscriber(
override val isDataCollectionEnabled: Boolean = true,
override val sessionSubscriberName: SessionSubscriber.Name = CRASHLYTICS,
) : SessionSubscriber {
override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) = Unit

val sessionChangedEvents = mutableListOf<SessionSubscriber.SessionDetails>()

override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) {
sessionChangedEvents.add(sessionDetails)
}
}
Loading

0 comments on commit 7a1b9f7

Please sign in to comment.