How to Test Contact List on Android (Complete Guide)

Contact lists are a gateway to personal data. When an Android app reads, writes, or shares contacts, any flaw can expose phone numbers, email addresses, or even notes that users consider private. A br

February 10, 2026 · 16 min read · How-To Guides

Why Contact List Testing Matters

Contact lists are a gateway to personal data. When an Android app reads, writes, or shares contacts, any flaw can expose phone numbers, email addresses, or even notes that users consider private. A broken contact picker may prevent a user from inviting friends, cause duplicate entries after a sync, or trigger a crash that leads to an ANR (Application Not Responding) report. In production, these issues translate into low ratings, support tickets, and, in regulated sectors, compliance risks.

Testing the contact list therefore serves three concrete goals:

  1. Data integrity – ensure that the app never corrupts the native contacts provider.
  2. User flow continuity – verify that actions such as picking a contact, adding a new one, or editing an existing entry complete without blocking the UI.
  3. Privacy and security – confirm that the app respects permission models, does not leak data to unintended components, and handles revoked permissions gracefully.

Because the contacts provider is a shared system component, bugs often surface only after interactions with other apps (e.g., a third‑party dialer or a backup service). Isolated unit tests miss these cross‑app effects, making a layered testing strategy essential.

---

Test Matrix for Contact List

The following table outlines a comprehensive set of scenarios grouped by functional area. Each row includes a short description, the expected outcome, and a priority label (P0 = blocker, P1 = high, P2 = medium). Use this matrix as a checklist when designing manual or automated test suites.

IDFunctional AreaScenarioDescriptionExpected ResultPriority
C1Happy‑path pickSingle contact selectionUser opens contact picker, taps one entry, confirms.Picker returns the selected contact’s lookupUri and display name; UI shows the chosen contact.P0
C2Happy‑path pickMultiple contact selectionUser enables multi‑pick, selects three contacts, confirms.Picker returns an array of three lookupUis; UI displays all three names.P0
C3Happy‑path addNew contact via intentApp launches ContactsContract.Intents.Insert.ACTION with pre‑filled name and phone.System shows add‑contact screen; after saving, app receives RESULT_OK and the new contact appears in the picker.P0
C4Happy‑path editEdit existing contactApp launches ACTION_EDIT on a contact URI; user changes phone number and saves.Contact provider reflects the updated number; subsequent picker shows the changed value.P0
C5Error handlingNo contacts availableDevice has zero contacts (new emulator or cleared provider).Picker displays empty state with a message like “No contacts”; pressing back returns to app without crash.P1
C6Error handlingPermission denied at runtimeApp requests READ_CONTACTS; user denies.Picker launches but system returns RESULT_CANCELED; app shows a permission rationale and does‑not‑have‑access toast and disables the button.P0
C7Error handlingPermission revoked while picker openUser grants permission, opens picker, then goes to Settings → Apps → [App] → Permissions and revokes READ_CONTACTS.Picker closes immediately; app receives RESULT_CANCELED and handles gracefully (no crash).P1
C8Data integrityDuplicate detectionApp inserts a contact with same name and phone as an existing entry; system is configured to merge duplicates.Provider creates a single contact; app’s query returns one entry with combined data (e.g., two phone numbers).P1
C9Data integrityConcurrent modificationWhile app reads contacts, a sync adapter adds a new contact in background.App’s query reflects the new contact after a requery; no CursorIndexOutOfBoundsException.P1
C10AccessibilityTalkBack navigationUser enables TalkBack, opens picker, navigates via swipe gestures.Each list item announces name, phone number, and “button, double tap to select”; selection works via double tap.P1
C11AccessibilityFont scalingUser sets system font size to 200%; opens picker.Text scales proportionally; no clipping or overlapping; touch targets remain ≥48 dp.P1
C12SecurityClipboard leakageUser long‑presses a phone number in picker; system offers “Copy”.Clipboard contains only the selected number; no extra metadata (e.g., raw lookupUri) is copied.P1
C13SecurityIntent redirectionApp exposes a picker result via setResult() without validating the URI.Malicious app cannot inject a content:// URI that points to a private provider; app validates scheme and authority before use.P0
C14PerformanceLarge contact listDevice has 10 000 contacts (simulated via adb shell content insert).Picker loads within 2 seconds; scrolling is smooth (≤16 ms frame drop).P2
C15PerformanceLow memoryDevice runs with <150 MB free RAM while picker is open.App does not crash; low‑memory warning is logged; UI remains responsive.P2
C16Cross‑app interactionShare contact via intentUser selects a contact, presses share, chooses “Message”.Messaging app opens with the contact’s number pre‑filled in the recipient field.P1
C17Cross‑app interactionQuick contact badgeLong‑press on a contact in picker shows quick‑action badge (call, message).Tapping badge launches the corresponding app with correct data; no permission prompt appears if already granted.P2
C18Sync conflictTwo accounts modify same contactGoogle account and Exchange account both have a contact named “Anna”; user edits phone on Google, server updates email on Exchange.After sync, contact contains both updates (merged); no data loss.P2
C19LocalizationRight‑to‑left languageSystem language set to Arabic (RTL).Picker layout mirrors; icons align correctly; touch targets remain functional.P1
C20Battery impactBackground service polling contactsApp starts a foreground service that queries contacts every 5 seconds.Battery historian shows no excessive wakelocks; service stops when app goes to background.P2

*Notes:*

---

Manual Testing Approach

Manual testing remains valuable for exploratory checks, accessibility validation, and verifying edge‑case interactions that automated scripts may overlook. Below is a step‑by‑step guide you can follow on a physical device or an emulator.

1. Environment Preparation

  1. Install Android Studio (or use the command‑line tools) and create an AVD with Google Play services (API 33 or higher).
  2. Enable Developer Options → USB debugging.
  3. Grant the app READ_CONTACTS, WRITE_CONTACTS, and android.permission.PROCESS_OUTGOING_CALLS if you plan to test call intents.
  4. Clear existing contacts (optional) via adb shell content delete --uri content://contacts/people/ to start from a clean slate.
  5. Install a contact‑populating script (see the “Contact Seeder” snippet later) if you need a deterministic set.

2. Test Execution Flow

StepActionExpected ObservationVerification Tips
1Launch the app, navigate to the contact‑picker screen.Picker appears with a list of contacts (or empty state).Use TalkBack to confirm each item is announced.
2Tap a single contact, confirm selection.App receives onActivityResult with RESULT_OK and a valid Uri.Log the returned Uri and compare to known contact ID.
3Long‑press a contact’s phone number, choose Copy.Clipboard holds exactly the number.After copying, open any text field and paste; verify no extra characters.
4Deny READ_CONTACTS when prompted.Picker returns RESULT_CANCELED; app shows a permission rationale.Check logs for SecurityException absence.
5Grant permission, open picker, then revoke via Settings while picker is open.Picker dismisses; app receives RESULT_CANCELED.Observe no crash in Logcat.
6Add a new contact via the “Add contact” FA button (if present).System add‑contact screen opens; after saving, the new entry appears in the picker.Verify the contact appears after a quick pull‑to‑refresh.
7Rotate device while picker is open.Picker retains scroll position and selection state.Ensure no flicker or reset to top.
8Enable Font size → 200% in Settings → Accessibility.All text scales; touch targets stay ≥48 dp.Use UI Automator Viewer to inspect bounds.
9Enable TalkBack, navigate via swipe.Each item announces name, number, and action; double‑tap selects.Confirm no missing announcements.
10Simulate low memory: run adb shell am kill [your.package] then immediately open picker.App recovers; picker loads without throwing OutOfMemoryError.Check Logcat for lowmemory signals.
11Install a second account (e.g., Exchange) and add a duplicate contact.Provider merges duplicates; picker shows a single entry with combined data.Open the contact in the native Contacts app to verify merge.
12Trigger a background sync while picker is open (e.g., force Google sync).New contact appears after a brief delay; no CursorIndexOutOfBoundsException.Use adb shell content query to monitor changes.
13Test with RTL language: set system language to Arabic.Layout mirrors; icons align correctly.Verify that the “Select” button is on the left side as expected.
14Measure performance: start picker, record frame timing with adb shell gfxinfo [package].Average frame time ≤16 ms; no jank spikes >64 ms.Look for GC pauses in the same output.

3. Handy Adb Commands for Manual Checks

4. When to Stop

If you have executed all rows in the matrix and observed the expected results, you can consider the manual pass complete. Any deviation—crash, ANR, incorrect return data, or accessibility failure—should be logged with steps, device model, Android version, and logcat excerpt before moving to automation.

---

Automated Testing Approaches

Automated tests give you repeatability and fast feedback. For contact‑list functionality, a combination of unit, instrumented UI, and API‑mocking tests provides the best coverage. The following sections detail each layer, with concrete code snippets you can copy into an Android Studio project.

1. Unit Tests – Repository Layer

Assuming you abstract contacts access behind a ContactRepository interface, you can test business logic without touching the provider. Use JUnit5 and Mockito (or MockK for Kotlin).


// ContactRepository.kt
interface ContactRepository {
    suspend fun getAllContacts(): List<ContactDto>
    suspend fun addContact(contact: ContactDto): Boolean
    suspend fun updateContact(contact: ContactDto): Boolean
}

// FakeContactRepository.kt (for unit tests)
class FakeContactRepository : ContactRepository {
    private val contacts = mutableListOf<ContactDto>()
    override suspend fun getAllContacts() = listOf(contacts)
    override suspend fun addContact(contact) {
        contacts += contact
        return true
    }
    override suspend fun updateContact(contact) {
        val idx = contacts.indexOfFirst { it.id == contact.id }
        if (idx >= 0) {
            contacts[idx] = contact
            return true
        }
        return false
    }
}

// ContactViewModelTest.kt
class ContactViewModelTest {
    private lateinit var viewModel: ContactViewModel
    private lateinit var repo: FakeContactRepository

    @BeforeEach
    fun setUp() {
        repo = FakeContactRepository()
        viewModel = ContactViewModel(repo)
    }

    @Test
    fun `addContact updates UI state`() = runTest {
        val newContact = ContactDto(id = 0, name = "Ada", phone = "5550001")
        viewModel.addContact(newContact)
        assertTrue(viewModel.uiState.value.contacts.contains(newContact))
    }
}

*Why unit tests?* They validate transformation logic (e.g., formatting phone numbers, handling nulls) instantly, without device boot‑time overhead.

2. Instrumented UI Tests – Espresso

Espresso excels at verifying UI interactions with the system contacts picker. Because the picker is an external activity, you need to use Intents to stub its result.

Add dependencies:


androidTestImplementation "android.support-lib:3.0androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1"
androidTestImplementation "androidx.test:rules:1.5.0"
androidTestImplementation "androidx.test.ext:junit:1.1.5"

#### Test: Happy‑path single pick


@RunWith(AndroidJUnit4::class)
class ContactPickerTest {

    @get:Rule
    val intentsRule = IntentsTestRule(MainActivity::class.java)

    @Test
    fun pickSingleContact_returnsResult() {
        // Prepare a dummy contact in the provider
        val values = ContentValues().apply {
            put(ContactsContract.Data.MIMETYPE,
                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, "Ada")
            put(ContactsContract.CommonDataKinds.Phone.NUMBER, "5559990000")
            put(ContactsContract.CommonDataKinds.Phone.TYPE,
                ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
        }
        val uri = InstrumentationRegistry.getInstrumentation()
            .targetContext
            .contentResolver
            .insert(ContactsContract.Data.CONTENT_URI, values)

        // Stub the picker activity to return our contact
        intending(
            hasAction(Intent.ACTION_PICK) &&
                hasData(ContactsContract.Contacts.CONTENT_URI)
        ).respondWith(
            Instrumentation.ActivityResult(Activity.RESULT_OK,
                Intent().putExtra(Intent.EXTRA_DATA, uri))
        )

        // Trigger the picker from UI
        onView(withId(R.id.btn_pick_contact))
            .perform(click())

        // Verify that the app shows the selected name
        onView(withId(R.id.tv_selected_name))
            .check(matches(withText("Ada")))
    }
}

#### Test: Permission denial


@Test
fun pickContact_whenPermissionDenied_showsRationale() {
    // Revoke permission before launching the activity
    grantPermission(
        InstrumentationRegistry.getInstrumentation().targetContext.packageName,
        Manifest.permission.READ_CONTACTS
    )
    revokePermission(
        InstrumentationRegistry.getInstrumentation().targetContext.packageName,
        Manifest.permission.READ_CONTACTS
    )

    onView(withId(R.id.btn_pick_contact))
        .perform(click())

    onView(withText("We need access to your contacts to pick one."))
        .check(matches(isDisplayed()))
}

*Tips*: Use grantPermission/revokePermission from androidx.test.core.app.ApplicationProvider to manipulate runtime permissions reliably.

3. API Mocking – MockWebServer

If your app syncs contacts with a backend, you can test error handling and merge logic without hitting a real server.


class ContactSyncTest {
    private lateinit var server: MockWebServer
    private lateinit var repo: ContactRepository

    @Before
    fun setUp() {
        server = MockWebServer()
        server.start()
        repo = ContactRepositoryImpl(
            apiService = Retrofit.Builder()
                .baseUrl(server.url("/"))
                .addConverterFactory(MoshiConverterFactory.create())
                .build()
                .create(ContactApi::class.java)
        )
    }

    @After
    fun tearDown() = server.shutdown()

    @Test
    fun sync_mergeHandlesDuplicate() = runBlocking {
        // Seed local contact
        repo.addContact(ContactDto(id = 1, name = "Bob", phone = "111"))

        // Server returns same contact with updated email
        server.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody("""[
                {"id":1,"name":"Bob","phone":"111","email":"bob@example.com"}
            ]"""))
        repo.syncFromServer()

        val contact = repo.getContactById(1)
        assertEquals("bob@example.com", contact?.email)
    }
}

4. Autonomous Exploration with SUSA

SUSA can be pointed at your APK (or a debug build) and will run a set of personas that exercise the contact list in ways scripted tests rarely consider.

CLI usage


# Install the agent (once)
pip install susatest-agent

# Run a 15‑minute exploratory session on a debug APK
susatest run \
    --apk path/to/app-debug.apk \
    --personas curious impatient novice adversarial elderly accessibility power_user \
    --duration 15m \
    --output susa-report.json

What SUSA does under the hood:

You can then commit those scripts to your repository and run them on every PR, gaining the advantage of *human‑like* exploration without maintaining the scenarios manually.

---

Edge Cases that Only Appear in Production

Even with a solid matrix and automation, certain bugs surface only after the app reaches real users. Below are the most common production‑only pitfalls for contact lists, along with detection strategies.

1. Contact Sync Conflicts Across Accounts

Users often have multiple accounts (Google, Exchange, corporate LDAP). When two accounts modify the same field simultaneously, the Android contacts provider uses a “last write wins” heuristic, which can cause data loss if your app does not respect the SYNC1 column or does not retry after a CONFLICT exception.

Detection

Mitigation

2. Runtime Permission Changes Triggered by OS Updates

Starting Android 12, the system can automatically revoke READ_CONTACTS if the app hasn’t used the permission for an extended period. If your app assumes the permission is permanently granted after the first dialog, it will suddenly start receiving SecurityException.

Detection

Mitigation

3. Contact Aggregation and Raw Contacts

The provider stores data in *raw contacts* (one per account) that are aggregated into a *display contact*. If your app queries only via ContactsContract.Contacts.CONTENT_URI without considering RAW_CONTACT_ID, you may miss updates that happen only in a specific raw contact (e.g., a phone number added to a work profile).

Detection

Mitigation

4. Accessibility Overlays (Font Size, Display Cutout)

On devices with large font settings or display cutouts (notches), the contact picker’s item layout can break: text may be clipped, or touch targets may shrink below the 48 dp minimum. These issues are only visible when the system UI is in a non‑default state.

Detection

Mitigation

5. Clipboard Data Leakage via Intent Extras

Some apps mistakenly place the entire Contact object (or a Cursor) into an intent extra when sharing a contact. On Android 13+, the system restricts access to extras from background apps, but a foreground malicious app could still read it if the permission is not properly protected.

Detection

Mitigation

6. Battery Drain from Background Polling

An app that polls the contacts provider every few seconds to show “frequent contacts” can cause excessive wake locks, leading to user complaints about battery drain.

Detection

Mitigation

---

Quick Checklist for Contact List Testing

AreaItemHow to Verify
PermissionsRequest at runtime, handle denial, handle revocation while UI openPermission dialog flows, Logcat for SecurityException
Happy PathPick single/multiple, add, edit, deleteReturned Uri, UI updates, provider reflects changes
Error StatesEmpty contacts, malformed Cursor, database lockedGraceful empty view, try/catch, retry logic
Data IntegrityDuplicate merging, raw‑contact aggregation, concurrent syncVerify merged fields, no lost data after sync
AccessibilityTalkBack navigation, font scaling, contrast, touch target sizeUse Accessibility Scanner, manual TalkBack walk‑through
SecurityClipboard content, intent URI validation, permission leakageClipboard read, intent extra inspection, grantUriPermission checks
PerformanceLoad time with 10k contacts, frame jank, low‑memory behaviorgfxinfo, adb shell meminfo, profiling with Android Studio Profiler
BatteryBackground polling vs observer, wakelock countbatterystats, Historian
LocalizationRTL layout, language‑specific text lengthSwitch system language, verify layout direction
Cross‑AppShare contact, quick‑action badges, intent forwardingTest with Messaging, Phone, Email apps
Automation CoverageUnit tests ≥80 %, Espresso tests for picker, ContentObserver testsRun ./gradlew test connectedAndroidTest and check coverage reports
Persona ExplorationRun SUSA or similar tool for at least 10 min per buildReview generated scripts for new failure patterns

Mark each item as PASS or FAIL after a test cycle; any FAIL should trigger a ticket before release.

---

Closing Takeaways

Testing a contact list on Android is more than verifying that a picker shows names. It is a multidimensional exercise that touches permissions, data integrity, accessibility, security, performance, and the complex interactions between your app, the system contacts provider, and other installed applications.

A solid testing strategy combines:

  1. Unit tests that validate pure logic (formatting, merging rules) without Android overhead.
  2. Instrumented UI tests (Espresso + Intents) that confirm the picker contract and permission handling.
  3. Contract/API mocks for server‑side sync scenarios, ensuring your merge logic behaves correctly when the network is unreliable.
  4. Manual exploratory sessions that catch layout glitches under extreme font sizes, RTL modes, or hardware quirks like notches.
  5. Autonomous, persona‑driven tools such as SUSA that surface edge cases only real users trigger—rapid taps, long presses, permission revocations mid‑flow, and multi‑account conflicts.

By following the matrix, the checklist, and the layered automation approach outlined here, you will reduce the risk of contact‑related bugs reaching production, protect user privacy, and maintain the trust users place in your app to handle one of their most sensitive data sets.

---

*Feel free to copy the code snippets, adapt the test matrix to your app’s specific fields, and integrate the SUSA CLI command into your CI pipeline for continuous, persona‑aware validation.*

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