Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion capture-sdk/sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ dependencies {
androidTestImplementation(libs.androidx.test.uiautomator)
androidTestImplementation(libs.mockito.android)
androidTestImplementation(libs.androidx.multidex)

testImplementation(libs.mockito.kotlin2)
androidTestImplementation(libs.mockito.kotlin2)
androidTestUtil(libs.androidx.test.orchestrator)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.gini.android.capture;

import androidx.annotation.Nullable;

/**
* Helper class to set the {@link GiniCapture} instance for instrumentation tests.
*/
public class GiniCaptureHelperForInstrumentationTests {
public static void setGiniCaptureInstance(@Nullable final GiniCapture giniCapture) {
GiniCapture.setInstance(giniCapture);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package net.gini.android.capture.ginicapture

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import androidx.fragment.app.testing.FragmentScenario
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import jersey.repackaged.jsr166e.CompletableFuture
import net.gini.android.capture.DocumentImportEnabledFileTypes
import net.gini.android.capture.EntryPoint
import net.gini.android.capture.GiniCapture
import net.gini.android.capture.GiniCaptureFragment
import net.gini.android.capture.GiniCaptureHelperForInstrumentationTests
import net.gini.android.capture.di.CaptureSdkIsolatedKoinContext
import net.gini.android.capture.internal.document.ImageMultiPageDocumentMemoryStore
import net.gini.android.capture.internal.network.Configuration
import net.gini.android.capture.internal.network.ConfigurationNetworkResult
import net.gini.android.capture.internal.network.NetworkRequestsManager
import net.gini.android.capture.internal.provider.GiniBankConfigurationProvider
import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker
import net.gini.android.capture.tracking.useranalytics.UserAnalytics
import net.gini.android.capture.view.DefaultLoadingIndicatorAdapter
import net.gini.android.capture.view.DefaultNavigationBarTopAdapter
import net.gini.android.capture.view.InjectedViewAdapterInstance
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.dsl.module
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import java.util.UUID

/**
* This class is responsible for testing the behavior of BufferedUserAnalyticsEventTracker
* in-case the user journey analytics is enabled or disabled from the Gini API.
* for more detailed information how Analytics works in this project,
* Please refer to the documentation written in the Unit tests of
* BufferedUserAnalyticsEventTracker
* [net.gini.android.capture.tracking.BufferedUserAnalyticsEventTrackerTest]
*
* */

@RunWith(AndroidJUnit4::class)
class GiniCaptureFragmentTest {
private lateinit var networkRequestsManager: NetworkRequestsManager
private lateinit var giniCapture: GiniCapture
private lateinit var giniInternal: GiniCapture.Internal
private lateinit var memoryStore: ImageMultiPageDocumentMemoryStore
private val koinTestModule = module {
single { GiniBankConfigurationProvider() }
}

/**
* We are using multiple dependencies in different classes, so many mocks are needed
* to fully test the functionality of AnalyticsTracker, by running the GiniCaptureFragment
* in Isolation.
* Mock was needed for [NetworkRequestsManager], [GiniCapture], [GiniCapture.Internal],
* [ImageMultiPageDocumentMemoryStore], [GiniBankConfigurationProvider]
* Also, in [GiniCaptureFragment], we are using koin to update
* the [GiniBankConfigurationProvider], for that we need to load and unload the module,
* other wise an exception will be thrown from koin for not loaded module.
* In the end, we need to set the mocked [GiniCapture] instance, and we have a helper class
* [GiniCaptureHelperForInstrumentationTests] for that.
*
* */

@Before
fun setUp() {
CaptureSdkIsolatedKoinContext.koin.loadModules(listOf(koinTestModule))

networkRequestsManager = mock()
giniCapture = mock()
giniInternal = mock()
memoryStore = mock()

whenever(giniInternal.networkRequestsManager).thenReturn(networkRequestsManager)
whenever(giniCapture.internal()).thenReturn(giniInternal)
whenever(giniCapture.entryPoint).thenReturn(EntryPoint.BUTTON)
whenever(giniInternal.imageMultiPageDocumentMemoryStore).thenReturn(memoryStore)
whenever(giniInternal.navigationBarTopAdapterInstance).thenReturn(
InjectedViewAdapterInstance(DefaultNavigationBarTopAdapter())
)
whenever(giniCapture.documentImportEnabledFileTypes).thenReturn(
DocumentImportEnabledFileTypes.NONE
)
whenever(giniCapture.internal().loadingIndicatorAdapterInstance).thenReturn(
InjectedViewAdapterInstance(DefaultLoadingIndicatorAdapter())
)

GiniCaptureHelperForInstrumentationTests.setGiniCaptureInstance(giniCapture)
}


/**
* Unload the koin modules which were loaded in the [setUp].
* */

@After
fun tearDown() = CaptureSdkIsolatedKoinContext.koin.unloadModules(listOf(koinTestModule))


@Test
fun analyticsTracker_shouldBeEmpty_whenUserJourneyDisabled() {

whenever(networkRequestsManager.getConfigurations(any())).thenReturn(
CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = false))
)

launchGiniCaptureFragment().use { scenario ->

scenario.moveToState(Lifecycle.State.STARTED)

scenario.onFragment { _ ->
assertThat(getAnalyticsTracker().getTrackers()).isEmpty()
}
}
}


@Test
fun analyticsTracker_shouldNotBeEmpty_whenUserJourneyEnabled() {

whenever(networkRequestsManager.getConfigurations(any())).thenReturn(
CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = true))
)

launchGiniCaptureFragment().use { scenario ->

scenario.moveToState(Lifecycle.State.STARTED)

scenario.onFragment { _ ->
assertThat(getAnalyticsTracker().getTrackers()).isNotEmpty()
}
}
}


private fun getMockedConfiguration(userJourneyEnabled: Boolean): ConfigurationNetworkResult {
val testConfig = Configuration(
id = UUID.randomUUID(),
clientID = TEST_CLIENT_ID,
isUserJourneyAnalyticsEnabled = userJourneyEnabled,
isSkontoEnabled = false,
isReturnAssistantEnabled = false,
isTransactionDocsEnabled = false,
isQrCodeEducationEnabled = false,
isInstantPaymentEnabled = false,
isEInvoiceEnabled = false,
amplitudeApiKey = TEST_API_KEY
)

return ConfigurationNetworkResult(testConfig, UUID.randomUUID())
}

private fun getAnalyticsTracker(): BufferedUserAnalyticsEventTracker {
return UserAnalytics.getAnalyticsEventTracker() as BufferedUserAnalyticsEventTracker
}

/**
* Helper method to launch the [GiniCaptureFragment] in a container,
* needed in all the tests.
*
* */

private fun launchGiniCaptureFragment(): FragmentScenario<GiniCaptureFragment> {
return FragmentScenario.launchInContainer(
fragmentClass = GiniCaptureFragment::class.java,
factory = object : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return GiniCaptureFragment.createInstance().apply {
setListener(mock())
}
}
}
)
}

companion object {
private const val TEST_CLIENT_ID = "test-client-id"
private const val TEST_API_KEY = "test-api-key"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.gini.android.capture.tracking.useranalytics

import android.content.Context
import androidx.annotation.VisibleForTesting
import net.gini.android.capture.internal.network.NetworkRequestsManager
import net.gini.android.capture.internal.provider.UniqueIdProvider
import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventProperty
Expand Down Expand Up @@ -141,4 +142,7 @@ internal class BufferedUserAnalyticsEventTracker(
eventTrackers.forEach(block)
}

@VisibleForTesting
internal fun getTrackers(): Set<UserAnalyticsEventTracker> = eventTrackers.toSet()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package net.gini.android.capture.tracking

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.mockk
import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker
import net.gini.android.capture.tracking.useranalytics.UserAnalyticsEvent
import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsUserProperty
import net.gini.android.capture.tracking.useranalytics.tracker.AmplitudeUserAnalyticsEventTracker
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.assertTrue
import org.junit.Assert.assertFalse

/**
* How do our analytics work?
*
* We are initializing the [BufferedUserAnalyticsEventTracker] and it adds all the trackers through
* [BufferedUserAnalyticsEventTracker.setPlatformTokens] into a set.
* If the config value (isUserJourneyEnabled) from the backend is false, no tracker should be
* added to the set, and eventually no event will be tracked.
*
* If the value is true, the passed tracker (e.g [AmplitudeUserAnalyticsEventTracker])
* will be added to the set and all the events will be tracked.
*
* This class will test this behaviour of [BufferedUserAnalyticsEventTracker]. If we pass false,
* No tracker should be added and vice versa.
*
* Things to consider:
* We are using queues to store the events, user properties and super properties.
* We can not test by checking these queues, because they are private, we can expose them for
* testing, but the main problem is that we are using polling for the queues
* in [BufferedUserAnalyticsEventTracker.trySendEvents] which empties the queues, so even if we
* expose them, it will be always empty after trackEvent is called,
* on the other hand, the trackers serve the same thing. If the tracker is empty, it
* already means that no events will be tracked. That's why every test of this class will check
* the trackers set. For all positive and negative cases.
*
* */

@RunWith(AndroidJUnit4::class)
class BufferedUserAnalyticsEventTrackerTest {

private lateinit var tracker: BufferedUserAnalyticsEventTracker
private val mockContext: Context = mockk(relaxed = true)
private val testSessionId = "test-session"
private val testApiKey = "test-api-key"
private val testCaptureVersion = "3.10.1"

@Before
fun setup() {
tracker = BufferedUserAnalyticsEventTracker(mockContext, testSessionId)
}

@Test
fun `when userJourney disabled, Initialize does not add trackers`() {
initializeTracker(isUserJourneyEnabled = false)

tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED)

assertTrue(tracker.getTrackers().isEmpty())
}


@Test
fun `when userJourney enabled, Initialize does add trackers`() {
initializeTracker(isUserJourneyEnabled = true)

tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED)

assertFalse(tracker.getTrackers().isEmpty())
}


@Test
fun `when userJourney disabled, setEventSuperProperty does not add trackers`() {
initializeTracker(isUserJourneyEnabled = false)

tracker.setEventSuperProperty(emptySet())

assertTrue(tracker.getTrackers().isEmpty())
}

@Test
fun `when userJourney enabled, setEventSuperProperty does add trackers`() {
initializeTracker(isUserJourneyEnabled = true)

tracker.setEventSuperProperty(emptySet())

assertFalse(tracker.getTrackers().isEmpty())
}

@Test
fun `when userJourney disabled, trackEvent does not add tracker`() {
initializeTracker(isUserJourneyEnabled = false)

tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet())

assertTrue(tracker.getTrackers().isEmpty())
}

@Test
fun `when userJourney enabled, trackEvent does add tracker`() {
initializeTracker(isUserJourneyEnabled = true)

tracker.trackEvent(UserAnalyticsEvent.SDK_OPENED, emptySet())

assertFalse(tracker.getTrackers().isEmpty())
}

private fun initializeTracker(isUserJourneyEnabled: Boolean) {

tracker.setPlatformTokens(
AmplitudeUserAnalyticsEventTracker.AmplitudeAnalyticsApiKey(testApiKey),
networkRequestsManager = mockk(relaxed = true),
isUserJourneyEnabled = isUserJourneyEnabled
)

tracker.setUserProperty(
setOf(
UserAnalyticsUserProperty.CaptureSdkVersionName(testCaptureVersion),
)
)
}
}