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
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:
- Data integrity – ensure that the app never corrupts the native contacts provider.
- 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.
- 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.
| ID | Functional Area | Scenario | Description | Expected Result | Priority |
|---|---|---|---|---|---|
| C1 | Happy‑path pick | Single contact selection | User opens contact picker, taps one entry, confirms. | Picker returns the selected contact’s lookupUri and display name; UI shows the chosen contact. | P0 |
| C2 | Happy‑path pick | Multiple contact selection | User enables multi‑pick, selects three contacts, confirms. | Picker returns an array of three lookupUis; UI displays all three names. | P0 |
| C3 | Happy‑path add | New contact via intent | App 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 |
| C4 | Happy‑path edit | Edit existing contact | App 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 |
| C5 | Error handling | No contacts available | Device 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 |
| C6 | Error handling | Permission denied at runtime | App 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 |
| C7 | Error handling | Permission revoked while picker open | User 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 |
| C8 | Data integrity | Duplicate detection | App 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 |
| C9 | Data integrity | Concurrent modification | While 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 |
| C10 | Accessibility | TalkBack navigation | User 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 |
| C11 | Accessibility | Font scaling | User sets system font size to 200%; opens picker. | Text scales proportionally; no clipping or overlapping; touch targets remain ≥48 dp. | P1 |
| C12 | Security | Clipboard leakage | User 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 |
| C13 | Security | Intent redirection | App 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 |
| C14 | Performance | Large contact list | Device has 10 000 contacts (simulated via adb shell content insert). | Picker loads within 2 seconds; scrolling is smooth (≤16 ms frame drop). | P2 |
| C15 | Performance | Low memory | Device runs with <150 MB free RAM while picker is open. | App does not crash; low‑memory warning is logged; UI remains responsive. | P2 |
| C16 | Cross‑app interaction | Share contact via intent | User selects a contact, presses share, chooses “Message”. | Messaging app opens with the contact’s number pre‑filled in the recipient field. | P1 |
| C17 | Cross‑app interaction | Quick contact badge | Long‑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 |
| C18 | Sync conflict | Two accounts modify same contact | Google 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 |
| C19 | Localization | Right‑to‑left language | System language set to Arabic (RTL). | Picker layout mirrors; icons align correctly; touch targets remain functional. | P1 |
| C20 | Battery impact | Background service polling contacts | App 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:*
- Priorities reflect impact on core user experience and compliance.
- Scenarios C14‑C20 are often missed by scripted tests because they depend on device state, system settings, or timing.
---
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
- Install Android Studio (or use the command‑line tools) and create an AVD with Google Play services (API 33 or higher).
- Enable Developer Options → USB debugging.
- Grant the app
READ_CONTACTS,WRITE_CONTACTS, andandroid.permission.PROCESS_OUTGOING_CALLSif you plan to test call intents. - Clear existing contacts (optional) via
adb shell content delete --uri content://contacts/people/to start from a clean slate. - Install a contact‑populating script (see the “Contact Seeder” snippet later) if you need a deterministic set.
2. Test Execution Flow
| Step | Action | Expected Observation | Verification Tips |
|---|---|---|---|
| 1 | Launch 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. |
| 2 | Tap 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. |
| 3 | Long‑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. |
| 4 | Deny READ_CONTACTS when prompted. | Picker returns RESULT_CANCELED; app shows a permission rationale. | Check logs for SecurityException absence. |
| 5 | Grant permission, open picker, then revoke via Settings while picker is open. | Picker dismisses; app receives RESULT_CANCELED. | Observe no crash in Logcat. |
| 6 | Add 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. |
| 7 | Rotate device while picker is open. | Picker retains scroll position and selection state. | Ensure no flicker or reset to top. |
| 8 | Enable Font size → 200% in Settings → Accessibility. | All text scales; touch targets stay ≥48 dp. | Use UI Automator Viewer to inspect bounds. |
| 9 | Enable TalkBack, navigate via swipe. | Each item announces name, number, and action; double‑tap selects. | Confirm no missing announcements. |
| 10 | Simulate 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. |
| 11 | Install 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. |
| 12 | Trigger 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. |
| 13 | Test 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. |
| 14 | Measure 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
- List contacts:
adb shell content query --uri content://contacts/people/ - Insert a test contact:
adb shell content insert \
--uri content://contacts/people/ \
--bind name:s:"Test User" \
--bind number:s:"5551234567"
adb shell content delete --uri content://contacts/people/
adb shell pm grant com.example.app android.permission.READ_CONTACTS
adb shell pm revoke com.example.app android.permission.READ_CONTACTS
adb logcat | grep -i "contacts\|content"
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:
- Launches the app on a device/emulator (or a fleet via Firebase Test Lab).
- For each persona, it generates a policy: e.g., the impatient persona taps rapidly, scrolls aggressively, and often cancels dialogs; the elderly persona uses longer press durations and prefers large touch targets.
- It monitors logs, ANR traces, and UI hierarchy changes, flagging any crash, dead button, or WCAG violation.
- After the run, it outputs a set of regression scripts (Appium for Android, Playwright for web views) that capture the exact interaction sequences that led to a failure.
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
- Run a test that creates a contact in Account A, then, without syncing, modifies the same contact in Account B via the native Contacts app.
- Trigger a sync from your app and verify that the final state contains both changes (or at least does not silently drop one).
Mitigation
- Listen for
ContentProviderOperationresults withBACK_REFERENCEto detect collisions. - Implement a retry with exponential backoff and surface a merge‑conflict UI to the user.
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
- Use
adb shell appops setto simulate a revocation after a period of idle time.READ_CONTACTS ignore - Launch the contact picker and assert that the app handles
RESULT_CANCELEDgracefully.
Mitigation
- Always check
ContextCompat.checkSelfPermissionright before launching the picker. - Cache the permission state and request again if denied, showing a rationale each time.
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
- Insert a raw contact with a phone number under a secondary account.
- Query using both the aggregate URI and the raw URI; confirm that your aggregation logic merges the data correctly.
Mitigation
- Use
ContactsContract.DatawithSELECT raw_contact_idto gather all pieces, then build your own aggregate model or rely onContactsContract.Contactswith the appropriate `HAS_PHOTO_* columns.
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
- Use the Accessibility Test Framework (ATF) to run UI Automator tests with
setFontScale(2.0f)andsetDisplayCutoutMode. - Verify that each item’s height stays ≥48 dp and that no
TextViewexceeds its parent bounds.
Mitigation
- Constrain item layouts with
ConstraintLayoutand useandroid:minHeight="48dp"for the root view. - Avoid hardcoded paddings; rely on
android:insetLeft/rightattributes that respect system insets.
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
- Use
adb shell dumpsys activity intentsafter triggering a share action; look for extras namedandroid.intent.extra.STREAMor custom parcels. - Ensure only primitive types (e.g.,
String,ArrayList) are present.
Mitigation
- When sharing, construct a new
IntentwithIntent.ACTION_SENDand put only the needed strings (EXTRA_TEXT,EXTRA_EMAIL,EXTRA_PHONE_NUMBER). - Call
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)to prevent leakage via task affinity.
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
- Use
adb shell cmd batterystats --resetthen run the app for 10 minutes with the polling service active. - Check
adb shell dumpsys batterystatsfor highWakeLockcount or longRunningtime.
Mitigation
- Replace polling with a
ContentObserverregistered viaContentResolver.registerContentObserver. - Unregister the observer in
onStop()or when the UI is backgrounded.
---
Quick Checklist for Contact List Testing
| Area | Item | How to Verify |
|---|---|---|
| Permissions | Request at runtime, handle denial, handle revocation while UI open | Permission dialog flows, Logcat for SecurityException |
| Happy Path | Pick single/multiple, add, edit, delete | Returned Uri, UI updates, provider reflects changes |
| Error States | Empty contacts, malformed Cursor, database locked | Graceful empty view, try/catch, retry logic |
| Data Integrity | Duplicate merging, raw‑contact aggregation, concurrent sync | Verify merged fields, no lost data after sync |
| Accessibility | TalkBack navigation, font scaling, contrast, touch target size | Use Accessibility Scanner, manual TalkBack walk‑through |
| Security | Clipboard content, intent URI validation, permission leakage | Clipboard read, intent extra inspection, grantUriPermission checks |
| Performance | Load time with 10k contacts, frame jank, low‑memory behavior | gfxinfo, adb shell meminfo, profiling with Android Studio Profiler |
| Battery | Background polling vs observer, wakelock count | batterystats, Historian |
| Localization | RTL layout, language‑specific text length | Switch system language, verify layout direction |
| Cross‑App | Share contact, quick‑action badges, intent forwarding | Test with Messaging, Phone, Email apps |
| Automation Coverage | Unit tests ≥80 %, Espresso tests for picker, ContentObserver tests | Run ./gradlew test connectedAndroidTest and check coverage reports |
| Persona Exploration | Run SUSA or similar tool for at least 10 min per build | Review 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:
- Unit tests that validate pure logic (formatting, merging rules) without Android overhead.
- Instrumented UI tests (Espresso + Intents) that confirm the picker contract and permission handling.
- Contract/API mocks for server‑side sync scenarios, ensuring your merge logic behaves correctly when the network is unreliable.
- Manual exploratory sessions that catch layout glitches under extreme font sizes, RTL modes, or hardware quirks like notches.
- 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