How to Test Location Services on Android (Complete Guide)

Location‑aware features are now a core part of many Android apps: navigation, ride‑hail, social check‑ins, fitness tracking, geo‑fencing, and targeted advertising. When the location subsystem fails, u

May 02, 2026 · 15 min read · How-To Guides

Why Location Services Testing Matters

Location‑aware features are now a core part of many Android apps: navigation, ride‑hail, social check‑ins, fitness tracking, geo‑fencing, and targeted advertising. When the location subsystem fails, users see wrong maps, missed check‑ins, inaccurate workout stats, or even privacy leaks. From a business perspective, a bad location experience can lead to abandoned flows, negative reviews, and regulatory scrutiny if personal data is mishandled.

Testing location services is more than verifying that a latitude/longitude pair is returned. You must validate:

If any of these areas is overlooked, the app may work in a controlled lab but fail when users move between indoor/outdoor environments, switch carriers, or enable battery‑saver modes. The following sections give a complete, practical guide to cover those risks.

---

Test Matrix for Location Services

The table below organizes tests by objective, description, expected outcome, and severity. Use it as a checklist when planning manual or automated suites.

#Test ObjectiveDescriptionExpected ResultSeverity
1Happy‑path GPS acquisitionLaunch app with location permission granted, ensure clear sky view (or use emulator GPS fix).Location callback returns a fix within 5 s, accuracy < 20 m, bearing updates on movement.Critical
2Permission denied flowDeny location permission at runtime.App shows a clear rationale, disables location‑dependent UI, does not crash.High
3Permission revoked while runningGrant permission, start location, then revoke via Settings while app is foreground.Location updates stop, app handles onProviderDisabled gracefully, no ANR.High
4Mock location detectionEnable developer options → “Select mock location app”, install a mock‑location provider, feed false coordinates.App either ignores mock location (if security‑sensitive) or shows a warning; never uses mock data for critical functions.Medium
5Network‑only providerDisable GPS, enable only Wi‑Fi/cellular location.App receives a coarse fix (accuracy ~ 500‑2000 m) within 10 s; UI reflects lower accuracy.Medium
6GPS signal loss simulationStart with GPS fix, then execute adb shell cmd location setgpsdisabled 1 or use emulator “GPS off”.Location callbacks stop, app shows stale‑data warning or falls back to network provider.Medium
7Background location limitsStart a foreground service that requests location, then press Home, enable battery optimization for the app.Location updates continue if a foreground service is used; otherwise updates stop after the system‑imposed interval (≈  few minutes).High
8Doze mode impactLeave device idle, unplugged, battery < 20 % to trigger Doze. Verify location request frequency.Requests are deferred to maintenance windows; app does not miss critical geo‑fence transitions if using FusedLocationProviderClient with setPriority(PRIORITY_BALANCED_POWER_ACCURACY).Low
9Battery consumption measurementUse adb shell dumpsys batterystats --reset, run location‑heavy scenario for 10 min, then adb shell dumpsys batterystats.Compute mA drain; ensure it stays within defined budget (e.g., < 150 mA for continuous high‑accuracy GPS).Low
10Accessibility of location errorsTrigger a location error (e.g., deny permission, then try to start navigation). Activate TalkBack.Error message is announced, focus moves to an actionable button (e.g., “Enable location”).Medium
11Security scan for location leakageRun a static analysis tool (e.g., MobSF) or manually inspect logs for Log.d containing latitude/longitude.No location data appears in plain‑text logs or crash reports.High
12Edge case: rapid provider switchingToggle GPS on/off every 2 seconds while app requests updates.App does not crash, does not flood the system with requests, respects minTime/minDistance parameters.Medium
13Edge case: simulated altitudeUse emulator GPS controls to set altitude to 0 m then 2000 m.App correctly receives altitude field (if requested) and does not treat it as error.Low
14Edge case: time‑travel testSet device clock far forward/backward while requesting location.Location timestamps remain monotonic; app does not rely on system clock for expiry logic.Low
15Edge case: external SD‑card storage of mock GPXPlace a GPX file on external storage, load it via a third‑party mock app.App handles malformed GPX gracefully (no crash).Low

How to use the matrix

---

Manual Testing Approach

Manual testing remains valuable for exploratory checks, especially when verifying hardware‑dependent behavior such as GPS drift or the effect of a case that blocks the antenna. Follow this step‑by‑step workflow on a physical device or an emulator.

1. Prepare the Device

  1. Enable Developer optionsUSB debugging.
  2. Under Developer options, turn on “Select mock location app” (we’ll use it later).
  3. Verify that Location is enabled in Settings → Location.
  4. Grant the test app location permission (if not already granted via adb: adb shell pm grant android.permission.ACCESS_FINE_LOCATION).

2. Baseline Happy‑Path Test

3. Simulate GPS Loss

4. Test Mock Location

5. Background Location & Battery Optimization

6. Doze Mode Validation

7. Accessibility Check

8. Battery Consumption Spot‑Check

9. Clean‑up

Manual testing gives you confidence that the hardware and system interactions behave as expected. The next sections show how to automate the same checks.

---

Automated Testing with Espresso/UIAutomator

Automated tests can run on every commit, catching regressions in permission handling, provider switching, and background behavior. Below are patterns that work reliably on Android instrumentation tests.

1. Mocking the LocationManager

Android’s LocationManager is a system service; you can replace it with a fake implementation using Mockito or a custom Shadow class if you use Robolectric. For instrumentation tests, the easiest approach is to bind a fake service via the @Before method.


@RunWith(AndroidJUnit4::class)
class LocationFeatureTest {

    private lateinit var locationManager: LocationManager
    private lateinit var fakeProvider: FakeLocationProvider

    @Before
    fun setUp() {
        val context = ApplicationProvider.getApplicationContext()
        locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager

        // Install a fake provider named "fake_gps"
        fakeProvider = FakeLocationProvider("fake_gps")
        locationManager.addTestProvider(
            fakeProvider.name,
            /*requiresNetwork=*/ false,
            /*requiresSatellite=*/ false,
            /*requiresCell=*/ false,
            /*hasMonetaryCost=*/ false,
            /*supportsAltitude=*/ true,
            /*supportsSpeed=*/ true,
            /*supportsBearing=*/ true,
            /*power=*/ Criteria.POWER_LOW,
            /*accuracy=*/ Criteria.ACCURACY_FINE
        )
        locationManager.setTestProviderEnabled(fakeProvider.name, true)
        locationManager.setTestProviderStatus(
            fakeProvider.name,
            LocationProvider.AVAILABLE,
            Bundle(),
            System.currentTimeMillis()
        )
    }

    @After
    fun tearDown() {
        locationManager.removeTestProvider(fakeProvider.name)
    }

    @Test
    fun `location request returns fake coordinates`() {
        val lifecycle = Scenario.launchActivity(MainActivity::class.java)
        lifecycle.onActivity { activity ->
            val fused = LocationServices.getFusedLocationProviderClient(activity)
            val listener = mock<LocationCallback>()
            fused.requestLocationUpdates(
                LocationRequest.create().apply {
                    interval = 500
                    priority = Priority.PRIORITY_HIGH_ACCURACY
                },
                listener,
                Looper.getMainLooper()
            )
            // Push a fake location
            fakeProvider.pushLocation(
                latitude = 37.422,
                longitude = -122.084,
                altitude = 0f,
                accuracy = 5f
            )
            // Allow time for callback
            Thread.sleep(2000)
            verify(listener, atLeastOnce()).onLocationChanged(argThat { loc ->
                loc.latitude == 37.422 && loc.longitude == -122.084
            })
        }
    }
}

FakeLocationProvider (simplified):


class FakeLocationProvider(private val name: String) {
    private var lastLocation: Location? = null

    fun pushLocation(latitude: Double, longitude: Double, altitude: Float, accuracy: Float) {
        val loc = Location(name).apply {
            setLatitude(latitude)
            setLongitude(longitude)
            setAltitude(altitude)
            setAccuracy(accuracy)
            setTime(System.currentTimeMillis())
        }
        lastLocation = loc
    }

    fun getLastLocation(): Location? = lastLocation
}

This test validates that the app correctly consumes location updates from a provider you control, without needing a real GPS fix.

2. Testing Permission Flows

Use ActivityScenario to launch the app with a specific permission state.


@Test
fun `denying location permission shows rationale`() {
    // Revoke permission before launch
    val context = ApplicationProvider.getApplicationContext()
    context.revokePermission(
        context.packageName,
        Manifest.permission.ACCESS_FINE_LOCATION
    )

    val scenario = Scenario.launchActivity(PermissionActivity::class.java)
    scenario.onActivity { activity ->
        // UI should show a rationale dialog
        onView(withId(R.id.location_rationale_dialog))
            .check(matches(isDisplayed()))
        // Clicking “Grant” should request permission
        onView(withText(R.string.grant)).perform(click())
        // Verify that the system permission dialog appears
        // (Espresso cannot interact with system dialogs; use UiDevice)
        val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
        uiDevice.waitForExists(
            UiObject2(By.text("Allow location access?")),
            5000
        )
    }
}

3. Background Location & Doze Simulation

You can use adb shell cmd job to trigger maintenance windows or rely on the AlarmManager to schedule a job that checks for location updates. In an automated test, you can advance the device clock:


@Test
fun `location updates continue in Doze when using foreground service`() {
    val scenario = Scenario.launchService(ForegroundLocationService::class.java)
    // Simulate Doze by forcing idle maintenance window
    val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
    uiDevice.executeShellCommand("cmd power set-idle true")
    uiDevice.executeShellCommand("cmd power step-idle")
    // Give the system a moment to process
    Thread.sleep(5000)

    // Verify that the service still received a location update
    val received = scenario.serviceLocationUpdates.takeIf { it.size > 0 } ?: emptyList()
    assertTrue(received.isNotEmpty())
}

4. Battery Impact Measurement in CI

While you cannot get precise mA values in a unit test, you can assert that the app does not exceed a reasonable number of location requests per minute:


@Test
fun `location request rate stays under threshold`() {
    val requestCounter = AtomicInteger(0)
    val locationCallback = object : LocationCallback() {
        override fun onLocationChanged(location: Location?) {
            requestCounter.incrementAndGet()
        }
    }

    val scenario = Scenario.launchActivity(TrackingActivity::class.java)
    scenario.onActivity { act ->
        val fused = LocationServices.getFusedLocationProviderClient(act)
        fused.requestLocationUpdates(
            LocationRequest.create().apply {
                interval = 2000 // 2 seconds
                priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY
            },
            locationCallback,
            Looper.getMainLooper()
        )
    }

    // Let it run for 20 seconds → expect ≤ 10 requests
    Thread.sleep(20_000)
    fused.removeLocationUpdates(locationCallback)
    assertTrue(requestCounter.get() <= 10, "Exceeded expected request rate")
}

These patterns give you a solid automated foundation. The next section shows how an autonomous, persona‑driven explorer can surface issues that scripted tests often miss.

---

Autonomous, Persona‑Driven Exploration with SUSA (SUSATest)

SUSA explores an app without predefined scripts, simulating real users with distinct behavior profiles. For location‑heavy features, this approach can reveal problems such as:

How to Run SUSA for Location Testing

  1. Install the agent

   pip install susatest-agent
  1. Point it at your APK or app URL

   susatest run \
       --apk path/to/app-release.apk \
       --personas curious impatient elderly adversarial \
       --location-mode gps \
       --duration 15m \
       --output ./susa-report
  1. What the report contains

Example Finding

During a recent run on a ride‑hail app, SUSA’s adversarial persona discovered that when mock location was enabled, the app still sent the falsified coordinates to its backend for fare calculation, allowing a tester to spoof a trip and receive a discount. The report flagged this as a High severity security issue, prompting the developers to add a source‑authenticity check (using Location.isFromMockProvider()).

Because SUSA varies personas, network states, and system settings autonomously, it catches edge cases like the combination of Doze mode + background location + battery optimization that a static test matrix might overlook unless explicitly scripted.

---

Edge Cases That Only Appear in Production

Even with thorough manual and automated suites, certain bugs surface only after the app reaches real users. Below are the most common production‑only location pitfalls and how to mitigate them.

IssueWhy It Happens in ProductionDetection Strategy
GPS drift in urban canyonsMultipath signals cause the reported position to jump tens of meters, confusing geo‑fence logic.Use a geo‑fence hysteresis (require the device to stay inside/outside for N seconds before triggering). Log the raw accuracy and speed to detect implausible jumps.
AGPS reliance on carrier dataSome networks suppress SUPL traffic, leading to longer TTFF (time to first fix) or reliance on cached coarse location.In the app, measure time from onLocationChanged to first high‑accuracy fix; if > 30 s on Wi‑Fi only, warn the user to enable mobile data or restart location services.
Battery optimizer killing foreground servicesManufacturers (e.g., Xiaomi, OnePlus) aggressively stop services even when marked foreground, especially if the app is not whitelisted.Provide a persistent notification with foreground service type FOREGROUND_SERVICE_TYPE_LOCATION and check ActivityManager.getRunningServices periodically; if the service disappears, prompt the user to add the app to the whitelist.
Doze + maintenance window misalignmentIf your app requests location every 5 minutes, it may miss the maintenance window and receive no updates for hours.Use FusedLocationProviderClient with setPriority(PRIORITY_BALANCED_POWER_ACCURACY) and setInterval(DEFAULT_INTERVAL_FASTEST to let the system batch requests; also schedule a WorkManager with setBackoffCriteria as a fallback.
Runtime permission revocation via system settingsUsers can disable location for an app while it’s in the background; the app may not receive onProviderDisabled if it relies only on a manifest‑declared receiver.Register a dynamic BroadcastReceiver for android.location.PROVIDERS_CHANGED in onStart/onStop and also listen to LocationManager.addGpsStatusListener.
External SD‑card mock GPX filesPower users place a GPX file on external storage and use a third‑party mock app that reads from the card; some mock apps fail to parse waypoint altitudes, causing the app to receive NaN values.Validate every received Location: `if (!location.hasAccuracy()location.getAccuracy() <= 0) { /* discard */ }`.
Time‑zone changes while trackingA user crosses a time‑zone border; the app uses System.currentTimeMillis() to timestamp logs, causing apparent backward jumps in location timelines.Store timestamps as elapsedRealtimeNanos() for internal calculations, converting to wall‑clock only for display.
Concurrent location clientsMultiple libraries (ads, analytics, maps) each request location with different priorities, leading to conflicting updates and excessive battery use.Centralize location requests through a singleton LocationRepository that merges requests and picks the highest priority; expose a removeAllListeners() method in onDestroy.
Incorrect handling of location.hasBearing()When the device is stationary, bearing may be unreliable; some apps rotate the map based on stale bearing, causing a jittery UI.Only update map orientation if location.hasBearing() and location.getSpeed() > 0.5 m/s.
Security leakage via third‑party SDKsAn ad SDK logs Location to its own server without the app’s knowledge.Run a network traffic inspection (e.g., using mitmproxy on a test device) and filter for any outbound requests containing lat/long parameters.

Mitigating these issues often requires a combination of defensive coding (validating every location field), thoughtful use of the fused provider APIs, and clear user communication when the system limits location access.

---

Checklist for Location Services Testing

Copy this list into your test‑plan wiki or CI README. Tick each item before a release.

---

Closing Takeaways

Location services are a common source of both functional bugs and privacy concerns. A robust testing strategy blends three layers:

  1. Manual exploratory checks that validate real‑world hardware interactions (GPS drift, mock‑location attacks, battery‑optimizer quirks).
  2. Automated instrumentation tests that assert permission flows, provider switching, background behavior, and request rates using Espresso/UIAutomator and mocked location providers.
  3. Autonomous, persona‑driven exploration (exemplified by SUSA) that surfaces rare combinations—such as an impatient user toggling mock location while the device is entering Doze— that scripted tests rarely consider.

By following the test matrix, applying the manual step‑by‑step routine, encoding the checks in automated suites, and leveraging an autonomous explorer for production‑like variability, you can catch the majority of location‑related defects before they reach users. Keep the checklist handy, monitor battery and permission metrics in every release, and treat location as a first‑class concern in your architecture—your users (and regulators) will thank you.

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free