Unit Testing with mocking of FusedLocationProviderClient and dependency injection with Koin
Asked Answered
R

2

7

Within our Android app we are using Google Maps API in order to show the users location. At the moment we are using Koin in order to provide a parametered injection (it requires an activity) of the FusedLocationProviderClient dependency into our Fragment class. In theory, this should make testing and mocking the Client class (with mockk) more straight forward. However, upon attempts to run tests with the Android FragmentScenario, it appears the test hangs in an endless loop somewhere (with debugging giving no answers as to why). Does anyone have any further ideas on how to test this alongside the frameworks we are using. Android/Google documentation offers no assistance and nor does numerous hours of trawling the web.

We have attempted to test with and without injection of the FusedLocationProvideClient. We have tried to launch the Koin test module, in our test class, parameterised with the activity and without and nothing seems to make a difference. The FusedLocationProviderClient is mocked with relaxed set to true.

Test Class:

private val viewModel = mockk<LocationSelectionViewModel>(relaxed = true)
    private val locationClient = mockk<FusedLocationProviderClient>(relaxed = true)


    override var testModule = module {
        viewModel { viewModel }
        factory { locationClient }
    }

    @Test
    fun itShouldDoSomethingWithLocation() {
        val scenario = FragmentScenario.launchInContainer(LocationSelectionDialogFragment::class.java)
        scenario.moveToState(Lifecycle.State.RESUMED)
        scenario.onFragment {
            val location = Location("this")
            location.latitude = 37.422
            location.longitude = -122.084
            location.accuracy = 3.0f

            locationClient.setMockMode(true)
            locationClient.setMockLocation(location)
            verify { viewModel.onDisplayed() }
        }
    }

Fragment Class:

class LocationSelectionDialogFragment: AbstractBottomSheetFragment(), KoinComponent, TestSupportDatabindingInterface, OnMapReadyCallback {

    private lateinit var _dataBinding: ViewDataBinding
    override fun dataBinding(): ViewDataBinding? = _dataBinding

    private val viewModel: LocationSelectionViewModel by viewModel()

    //Ma Objects n Variables
    private val LOCATION_PERMISSION = 42

    private lateinit var map: GoogleMap
    private var mapView: MapView? = null

    private val fusedLocationClient: FusedLocationProviderClient by inject { parametersOf(activity!!) }
    private lateinit var locationCallback: LocationCallback

Stuck in an endless loop

Ridenour answered 16/7, 2019 at 8:45 Comment(2)
Did you solved your issue? I'm facing the same problem at the momentMcnamee
try to move verify outside onFragment bracketsUnworldly
S
0

If you're attempting to mock one of the Task<Location> objects returned by the FusedLocationProviderClient and your application code uses Coroutines and calls the await() method on that Task, then you'll need to use a static mock:

mockkStatic("kotlinx.coroutines.tasks.TasksKt")

There are several other Task extension methods in that file, so this should fix a similar situation with those as well.

Shove answered 2/5, 2023 at 19:53 Comment(0)
S
0

This might help someone in the future, if you're using FusedLocationProviderClient to get the location and mockk for testing, you can use slot, here's a full example with test:

1. Data class for longitude and latitude

data class LongLat(
    val latitude: Double,
    val longitude: Double,
)

2. Domain interface for location permission and getting LongLat

interface LocationManager {
    fun isLocationPermissionGranted(): Boolean
    fun getLongLat(): Flow<LongLat>
}

3. DI

@InstallIn(SingletonComponent::class)
@Module
object LocationProviderClientModule {
    @Provides
    @Singleton
    fun providesFusedLocationProviderClient(@ApplicationContext context: Context): FusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(context)
}

4. Data implementation

class LocationManagerImpl @Inject constructor(
    @ApplicationContext val context: Context,
    private val fusedLocationProviderClient: FusedLocationProviderClient,
) : LocationManager {

    override fun isLocationPermissionGranted(): Boolean =
        PackageManager.PERMISSION_GRANTED == context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION)

    @SuppressLint("MissingPermission")
    @Throws
    override fun getLongLat(): Flow<LongLat> {
        val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
            .setMinUpdateDistanceMeters(1000F)
            .build()
        return callbackFlow {
            if (!isLocationPermissionGranted()) throw Exception("Location permission is not granted!")
            val locationCallback = object : LocationCallback() {
                override fun onLocationResult(result: LocationResult) {
                    super.onLocationResult(result)
                    result.lastLocation?.let { location ->
                        launch {
                            send(
                                LongLat(
                                    latitude = location.latitude,
                                    longitude = location.longitude,
                                ),
                            )
                        }
                    }
                }
            }

            fusedLocationProviderClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper())

            awaitClose {
                fusedLocationProviderClient.removeLocationUpdates(locationCallback)
            }
        }
    }
}

5. Test case for getLongLat

private lateinit var classUnderTest: LocationManagerImpl

@MockK
private lateinit var context: Context

@MockK
private lateinit var fusedLocationProviderClient: FusedLocationProviderClient

@Before
fun setup() {
    MockKAnnotations.init(this)
    classUnderTest = LocationManagerImpl(
        context = context,
        fusedLocationProviderClient = fusedLocationProviderClient,
    )
}

@Test
fun given_ACCESS_COARSE_LOCATION_is_granted_when_getLongLat_then_LongLat_as_expected() = runTest {
    val expected = LongLat(
        latitude = 25.3874024,
        longitude = 51.5189585,
    )
    every { context.checkSelfPermission(ACCESS_COARSE_LOCATION) } returns 0
    val locationMock = mockk<Location>(relaxed = true)
    every { locationMock.latitude } returns expected.latitude
    every { locationMock.longitude } returns expected.longitude

    val locationResultMock = mockk<LocationResult>(relaxed = true)
    every { locationResultMock.lastLocation } returns locationMock

    val slot = slot<LocationCallback>()
    every {
        fusedLocationProviderClient.requestLocationUpdates(any(), capture(slot), any())
    } returns mockk()
    every {
        fusedLocationProviderClient.removeLocationUpdates(capture(slot))
    } returns mockk()

    val emittedValues = mutableListOf<LongLat>()
    val job = classUnderTest.getLongLat().onEach {
        emittedValues.add(it)
    }.launchIn(this + UnconfinedTestDispatcher(testScheduler))
    slot.captured.onLocationResult(locationResultMock)

    val actual = emittedValues.lastOrNull()
    assertEquals(expected, actual)

    verify(exactly = 1) { fusedLocationProviderClient.requestLocationUpdates(any(), capture(slot), any()) }
    job.cancel()
}
Short answered 3/10, 2024 at 8:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.