How to Test Favorites on Android (Complete Guide)
Favorites—whether they are starred articles, saved playlists, bookmarked locations, or pinned contacts—are a core engagement mechanism in many Android apps. Users expect the action to be instantaneous
Why Testing Favorites on Android Deserves Dedicated Attention
Favorites—whether they are starred articles, saved playlists, bookmarked locations, or pinned contacts—are a core engagement mechanism in many Android apps. Users expect the action to be instantaneous, persistent across device restarts, and correctly reflected wherever the item appears. When the favorite flow breaks, the symptom is often subtle: a missing icon, a stale badge, or a silent failure to persist data. Those symptoms can erode trust, increase churn, and generate support tickets that are hard to trace because the failure may only manifest under specific conditions such as low storage, background sync throttling, or a particular Android version.
Testing favorites therefore needs to go beyond a single “tap the star and verify it’s filled” check. A robust strategy must cover data persistence, UI synchronization, concurrency, accessibility, and privacy implications. The following sections lay out a complete, practical guide that you can apply to any Android app, from a simple news reader to a complex e‑commerce platform.
---
Understanding How Favorites Are Typically Implemented
Before designing tests, it helps to know the common patterns you will encounter. Most apps store the favorite state in one of three places:
| Storage Layer | Typical APIs | Pros | Cons |
|---|---|---|---|
| SharedPreferences | getSharedPreferences(), edit().putBoolean() | Simple, fast, private to the app | No built‑in sync, limited to primitive types |
| SQLite / Room | @Entity, DAO methods | Structured queries, migration support | Requires schema management, more boilerplate |
| Remote backend (REST/GraphQL) | Retrofit, Coroutines, LiveData | Enables cross‑device sync, server‑side validation | Network latency, offline handling complexity |
The UI usually toggles a drawable (filled vs. outline star) and updates a counter or badge. Some implementations use MenuItem.setIcon() in the action bar, others use a custom ImageView inside a RecyclerView item. Regardless of the visual representation, the underlying state change follows the same flow:
- User interaction (click, long press, swipe) →
- ViewModel / Presenter toggles a boolean flag →
- Flag persisted (local or remote) →
- UI observes the change and redraws the favorite indicator.
Knowing where each step lives lets you target unit, integration, and UI tests appropriately.
---
Comprehensive Test Matrix for Favorites
The table below enumerates the scenarios you should verify. Each row groups related test ideas; the columns indicate the test type (manual, automated unit, automated UI) and the expected outcome.
| # | Scenario | Description | Manual | Unit Test | UI Test (Espresso/UIAutomator) | Expected Result |
|---|---|---|---|---|---|---|
| 1 | Happy path toggle | User taps favorite icon on an item that is not favored; icon fills, badge increments. | ✔ | ✔ (ViewModel toggle) | ✔ (click + assert drawable) | State persisted, UI updated instantly |
| 2 | Unfavorite | User taps filled icon; icon reverts to outline, badge decrements. | ✔ | ✔ | ✔ | State cleared, UI updated |
| 3 | Rapid double‑tap | Two taps within 200 ms (debounce test). | ✔ | — | ✔ (use performClick() twice with Thread.sleep(10)) | Only one state change occurs |
| 4 | Toggle while item is off‑screen (RecyclerView) | Scroll list, favorite an item that is currently recycled, then scroll back. | ✔ | — | ✔ (scrollToPosition, click, scrollBack, assert) | State retained, correct icon shown |
| 5 | Configuration change | Rotate device or change language while favorite is pending. | ✔ | ✔ (ViewModel survives) | ✔ (rotate, assert UI) | No loss of state, UI consistent |
| 6 | Process kill & restore | Swipe app from recent, relaunch; favorite state must be restored. | ✔ | ✔ (Repository load) | ✔ (kill via adb shell am force-stop, relaunch, assert) | Persisted state survives |
| 7 | Low storage condition | Device storage < 10 MB; attempt to favorite. | ✔ | — | ✔ (use adb shell pm set-install-location 2 to force internal, fill storage) | App handles error gracefully (toast, no crash) |
| 8 | Network loss (remote sync) | Disable Wi‑Fi/mobile data before toggling a server‑backed favorite. | ✔ | ✔ (Repository returns error) | ✔ (toggle, assert offline UI, then re‑enable network, assert sync) | Local optimistic update, retry on reconnect |
| 9 | Conflict resolution (server‑side reject) | Server returns 409 when trying to favorite an already‑favored item (race). | ✔ | ✔ (ViewModel handles error) | ✔ (mock server with WireMock, assert error UI) | UI reverts to previous state, error shown |
| 10 | Accessibility – content description | Favorite icon announces state correctly for TalkBack. | ✔ (use accessibilityLiveRegion | TalkBack reads “favorite, selected” or “not favorite” | ||
| 11 | Touch target size | Minimum 48 dp touch area around icon. | ✔ | — | ✔ (use UiAutomator to get bounds) | Hit‑test passes |
| 12 | Color contrast | Icon meets WCAG AA (≥ 4.5:1) against background. | ✔ | — | ✔ (use Android Studio’s Accessibility Scanner) | Contrast ratio compliant |
| 13 | Data leakage – logs | No favorite identifiers appear in Logcat at VERBOSE level. | ✔ | — | — (inspect adb logcat) | No PII in logs |
| 14 | Permission misuse | App does not request unnecessary permissions (e.g., READ_CONTACTS) just to store favorites. | ✔ | — | — (check manifest) | No extraneous permissions |
| 15 | Backup/restore (Android Auto‑Backup) | Favorite state restored after a factory reset via ADB backup. | ✔ | — | — (run adb backup, wipe data, adb restore) | State present after restore |
| 16 | Multi‑user / profile | Favorite state isolated between work profile and personal profile. | ✔ | — | — (switch user, test) | No cross‑profile leakage |
| 17 | Battery optimization whitelist | App continues to sync favorites in Doze mode if required. | ✔ | — | — (use adb shell dumpsys battery unplug) | Sync occurs as expected |
| 18 | Stress – rapid toggling | 100 rapid toggles via automation; verify final state matches parity. | ✔ | — | ✔ (loop 100 clicks) | No crash, final state correct |
| 19 | Localization – RTL layout | Favorite icon mirrors correctly in right‑to‑left languages. | ✔ | — | ✔ (set locale to ar-EG, assert layout) | Icon positioned correctly |
| 20 | Dark mode | Icon retains proper contrast in night mode. | ✔ | — | ✔ (enable night mode, assert) | Visual consistency |
*Notes*:
- Unit tests target the ViewModel/Repository layer; UI tests use Espresso for synchronized UI interactions or UIAutomator2 for cross‑app scenarios (e.g., checking notifications).
- Manual checks are still valuable for exploratory steps, visual verification, and accessibility tooling.
- The matrix can be expanded with app‑specific flows (e.g., favoriting a product then proceeding to checkout).
---
Manual Step‑by‑Step Testing Approach
Even when you have automated suites, a disciplined manual pass catches regressions that scripts overlook, especially those tied to device state or OS behavior. Follow this checklist on a physical device (or emulator with Google Play services) for each build:
- Preparation
- Install the app via
adb install -r app.apk. - Clear data:
adb shell pm clear com.example.app. - Grant any runtime permissions the app requests (location, storage) via Settings → Apps → Permissions.
- Set device language to English (US) and then later to a RTL language (Arabic) for localisation checks.
- Baseline Navigation
- Launch the app, navigate to a screen that displays a list of items with favorite icons (e.g., article feed).
- Verify that all icons start in the unfilled state and that any badge shows zero.
- Happy Path Toggle
- Tap the first item’s favorite icon.
- Observe immediate visual change (filled icon) and badge increment.
- Navigate away (home, recent apps) and return; confirm the icon remains filled.
- Unfavorite
- Tap the same icon again; confirm outline returns and badge decrements.
- Rapid Interaction
- Using two fingers, tap the icon twice as fast as possible.
- Verify only one state change occurred (no double increment).
- Scroll‑and‑Favorite
- Scroll down until the fifth item is off‑screen.
- Favorite that item via the overflow menu or long‑press if the icon is not visible.
- Scroll back up; the item must show as favored.
- Configuration Change
- While an item is favored, rotate the device to landscape.
- Confirm the icon stays filled and the badge is correct.
- Change font size (Settings → Accessibility → Font size) to largest; ensure icons still fit and are readable.
- Process Kill
- Open Recent Apps, swipe the app away.
- Relaunch from launcher; verify favored items persist.
- Low Storage Simulation
- On emulator:
adb shell pm set-install-location 2(force internal). - Fill storage:
adb shell dd if=/dev/zero of=/data/local/tmp/fill bs=1M count=400. - Attempt to favorite an item; app should show a toast like “Unable to save favorite – low storage” and not crash.
- Clean up:
adb shell rm /data/local/tmp/fill.
- Network Loss & Recovery
- Turn off Wi‑Fi and mobile data.
- Favorite a server‑backed item; expect an optimistic UI change plus a toast or snackbar indicating “Will sync when online”.
- Re‑enable network; wait a few seconds; confirm the favorite appears on the server (check via backend tool or log).
- Accessibility Check
- Enable TalkBack.
- Focus on a favorite icon; listen for announcement: “Favorite, selected” or “Favorite, not selected”.
- Disable TalkBack and run Android Studio’s Accessibility Scanner; note any contrast or touch‑target warnings.
- Permission & Log Hygiene
- Review
AndroidManifest.xmlfor any permission not obviously needed for favorites. - With the app running, execute
adb logcat -s *:Vand perform a few favorite toggles; ensure no user‑identifiable strings (IDs, names) appear in the log.
- Backup/Restore
- Create a backup:
adb backup -f fav.ab -noapk com.example.app. - Wipe data:
adb shell pm clear com.example.app. - Restore:
adb restore fav.ab. - Relaunch app; verify previously favored items are still marked.
- Multi‑User Test (if device supports)
- Add a new user via Settings → System → Multiple users.
- Switch to the new user, install the app, and confirm no favorites from the primary user appear.
- Switch back; primary user’s favorites should be intact.
- Battery Optimization
- Whitelist the app from battery optimization: Settings → Apps → → Battery → Unrestricted.
- Enable Doze mode (
adb shell dumpsys deviceidle force-idle). - Trigger a favorite that requires server sync; confirm the sync still occurs (check network logs).
- Stress Test
- Use a simple script (see Automation section) to toggle the same item 150 times rapidly.
- Observe that the app stays responsive and the final state matches the parity of clicks (odd → favored, even → not).
Following these steps on every release candidate gives you confidence that the favorite mechanism behaves correctly under typical and atypical conditions.
---
Automated Approaches and Tooling
1. Unit Testing the Business Logic
Use JUnit 4 (or JUnit 5 with AndroidJUnitRunner) combined with Mockito to verify ViewModel and Repository behavior.
@RunWith(MockitoJUnitRunner::class)
class FavoriteViewModelTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@Mock
private lateinit var favoriteRepository: FavoriteRepository
private lateinit var viewModel: FavoriteViewModel
@Before
fun setUp() {
viewModel = FavoriteViewModel(favoriteRepository)
}
@Test
fun `toggle favorite updates repository and livedata`() {
val itemId = "article_42"
// Initially not favored
whenever(favoriteRepository.isFavorite(itemId)).thenReturn(false)
viewModel.toggleFavorite(itemId)
verify(favoriteRepository).setFavorite(itemId, true)
assertTrue(viewModel.favoriteState(itemId).getValueOrNull())
}
@Test
fun `double toggle reverts state`() {
val itemId = "video_7"
whenever(favoriteRepository.isFavorite(itemId)).thenReturn(true)
viewModel.toggleFavorite(itemId) // unfavorite
viewModel.toggleFavorite(itemId) // favorite again
verify(favoriteRepository, times(2)).setFavorite(eq(itemId), anyBoolean())
assertTrue(viewModel.favoriteState(itemId).getValueOrNull())
}
}
*Why this matters*: Unit tests catch logic errors (e.g., forgetting to invert the boolean) before they reach the UI layer, providing fast feedback during CI.
2. Espresso UI Tests for Synchronized Interactions
Espresso shines when you need to assert UI changes that are tightly coupled to the activity lifecycle.
@LargeTest
@RunWith(AndroidJUnit4::class)
class FavoriteUiTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class)
@Test
fun favoriteTogglePersistsAfterRotation() {
// Click first item's favorite button (assuming id R.id.fav_btn)
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.actionOnItemAtPosition<ItemViewHolder>(
0,
ViewActions.click()
))
// Verify filled star
onView(withId(R.id.fav_btn))
.check(matches(hasDrawable(R.drawable.ic_star_filled)))
// Rotate device
activityRule.scenario.onActivity { activity ->
activity.setRequestedOrientation(
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
)
}
// Assert still filled
onView(withId(R.id.fav_btn))
.check(matches(hasDrawable(R.drawable.ic_star_filled)))
}
// Helper matcher
private fun hasDrawable(@DrawableRes resId: Int): Matcher<View> {
return object : BoundedMatcher<View, ImageView>(ImageView::class.java) {
override fun describeTo(description: Description) {
description.appendText("has drawable $resId")
}
override fun matchesSafely(item: ImageView): Boolean {
return item.drawable?.constantState ==
ContextCompat.getDrawable(item.context, resId)?.constantState
}
}
}
}
Espresso automatically waits for the UI thread to be idle, making it reliable for checking that a click results in an immediate drawable swap.
3. UIAutomator2 for Cross‑App and System‑Level Scenarios
When you need to interact with the recent‑apps tray, system dialogs, or verify that a favorite persists after a force‑stop, UIAutomator2 is the right choice.
@RunWith(AndroidJUnit4.class)
public class FavoriteUiAutomatorTest {
private UiDevice device;
@Before
public void setUp() {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
@Test
public void favoriteSurvivesForceStop() throws Exception {
// Launch app
Context ctx = InstrumentationRegistry.getInstrumentation().getTargetContext;
Intent intent = ctx.getPackageManager()
.getLaunchIntentForPackage(ctx.getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
ctx.startActivity(intent);
// Favorite first item in list (assume resource id)
UiObject2 favBtn = device.findObject(By.res(ctx.getPackageName(), "fav_btn")
.child(By.clazz(android.widget.ImageView.class)));
favBtn.click();
// Force stop via ADB (UIAutomator cannot directly kill, so use shell)
device.executeShellCommand("am force-stop " + ctx.getPackageName());
// Relaunch
ctx.startActivity(intent);
// Verify still favored
UiObject2 favBtnAfter = device.findObject(By.res(ctx.getPackageName(), "fav_btn")
.child(By.clazz(android.widget.ImageView.class)));
assertTrue(favBtnAfter.getDrawable().getConstantState()
.equals(ctx.getDrawable(R.drawable.ic_star_filled).getConstantState()));
}
}
This test validates that the favorite state survives a process kill—a scenario that unit tests cannot cover.
4. Appium for Hybrid or Web‑View Favorites
If part of your favorite UI lives inside a WebView (e.g., a product page loaded from a remote server), Appium lets you drive both native and web contexts.
@Test
public void favoriteInWebView() throws Exception {
AndroidDriver<MobileElement> driver = new AndroidDriver<>(new URL("http://127.0.0.1:4723/wd/hub"), caps);
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
// Navigate to product page
driver.findElement(By.id("open_product")).click();
// Switch to WebView
Set<String> contexts = driver.getContextHandles();
for (String ctx : contexts) {
if (ctx.contains("WEBVIEW")) {
driver.context(ctx);
break;
}
}
// Click the star icon inside the web view
driver.findElement(By.cssSelector(".favorite-icon")).click();
// Verify class changed to favored
String favClass = driver.findElement(By.cssSelector(".favorite-icon"))
.getAttribute("class");
assertTrue(favClass.contains("favored"));
// Return to native
driver.context("NATIVE_APP");
}
Appium is slower than Espresso but indispensable when the favorite control is rendered via HTML/CSS/JS.
5. Leveraging SUSA for Autonomous, Persona‑Driven Exploration
SUSA (susatest.com) can be pointed at your APK or a deep‑link URL and will explore the app using a variety of user personas—curious, impatient, novice, accessibility‑focused, power‑user, and adversarial. Each persona has a distinct behavior profile (e.g., the impatient persona taps rapidly and abandons slow screens; the accessibility persona relies on TalkBack and explores via swipe gestures).
During a run, SUSA automatically:
- Records every screen visited and every action taken.
- Detects crashes, ANRs, unhandled exceptions, and dead UI elements.
- Checks WCAG contrast and touch‑target size on the fly.
- Flags attempts to write sensitive identifiers to logs.
- Generates regression scripts in Appium (Android) and Playwright (Web) that you can add to your CI pipeline.
Because SUSA does not rely on pre‑written test cases, it often discovers favorite‑related bugs that scripted tests miss—such as a race condition that only appears when the novice persona repeatedly taps the favorite button while a network request is still pending, or an accessibility flaw where the favorite icon lacks a content description when the screen is rendered in a custom view hierarchy.
To run SUSA locally:
pip install susatest-agent
susatest run \
--apk path/to/app.apk \
--personas curious impatient accessibility \
--output-dir ./susa-reports \
--export-appium ./generated-tests
The generated Appium tests can be inspected, refined, and committed alongside your manual Espresso suite, giving you a hybrid approach that combines deterministic verification with exploratory, persona‑driven discovery.
---
Edge Cases That Only Surface in Production
Even the most thorough lab testing can miss issues that arise only under real‑world conditions. Below are several production‑only failure modes specific to favorites, along with detection strategies.
| Failure Mode | Root Cause | How to Observe in Prod | Mitigation |
|---|---|---|---|
| Delayed persistence due to I/O throttling | Android’s background I/O limits (e.g., on low‑end devices) delay writes to SharedPreferences or Room. | Use Firebase Performance Monitoring to trace the time between toggleFavorite call and the subsequent SharedPreferences.commit() seen in logs. | Prefer apply() for asynchronous writes when UI does not need immediate confirmation; otherwise, show a loading indicator until the write completes. |
| Sync conflict after offline edit | User favorites an item while offline; server later receives a conflicting update from another device. | Monitor backend logs for 409 Conflict responses on favorite endpoints; correlate with device offline events from analytics. | Implement client‑side conflict resolution (last‑write‑wins with timestamp or merge strategy) and surface a non‑intrusive snackbar when a conflict is resolved. |
| Favorite badge not updating in notification shade | A RemoteViews‑based notification is not refreshed after the favorite state changes. | Check notification shade screenshots from user bug reports; look for stale badge numbers. | After persisting the favorite, call NotificationManager.notify() with an updated RemoteViews built from the latest state. |
| Data loss after app update | Migration script fails to copy the old favorite table to the new Room schema. | Compare favorite counts pre‑ and post‑update via remote config flag; spikes indicate loss. | Write automated migration tests that run on a copy of the production database; use Room’s MigrationTestHelper. |
| Excessive battery drain from polling | App polls server every few seconds to reconcile favorite state instead of using push. | Use Battery Historian (adb bugreport) to identify frequent wakelocks tied to the favorite sync service. | Switch to Firebase Cloud Messaging or WorkManager with setExpedited(true) only when immediate sync is required. |
| Incorrect favorite state after Android “App standby bucket” restrictions | App placed in standby bucket defers background jobs, causing delayed sync. | Observe that favorites made while the app is in the background only appear after the user launches the app again. | Use WorkManager with setExpedited(true) for user‑initiated favorites and respect bucket constraints for background reconciliations. |
| Leak of favorite IDs via clipboard | Debug code copies the item ID to clipboard when the star is long‑pressed. | Monitor clipboard contents via adb shell service call clipboard 1 i32 0 after a long press. | Strip any clipboard writes from release builds; enable StrictMode or use a release‑only ProGuard rule to strip the method. |
| Favorite icon disappears in split‑screen mode | Layout uses match_parent width on a container that gets zero width in multi‑window. | User reports missing star when using split‑screen; verify via UI Automator in split‑screen mode. | Constrain favorite icons with explicit dimensions or use ConstraintLayout with android:layout_gravity="end" that respects window metrics. |
Detecting these issues often requires a combination of remote analytics (Firebase, Sentry), custom logging (guarded by build variants), and periodic manual spot‑checks on a fleet of real devices (e.g., using Firebase Test Lab’s device catalog).
---
Quick Reference Checklist
Copy this list into your team’s wiki or a Markdown file for easy reference before each release.
[ ] Verify favorite toggle updates UI instantly (filled/unfilled icon, badge change)
[ ] Confirm state survives:
- Device rotation
- Font size change
- Process kill / force stop
- Low‑storage condition
- Backup / restore flow
[ ] Test rapid double‑tap → single state change
[ ] Validate favorite works when item is off‑screen (RecyclerView reuse)
[ ] Ensure accessibility:
- Content description reflects state
- Minimum 48 dp touch target
- WCAG AA contrast (≥ 4.5:1) in light & dark mode
[ ] Check no PII appears in Logcat at VERBOSE level
[ ] Confirm no extraneous permissions requested for favorites
[ ] Verify offline optimistic update + retry on reconnect (if server‑backed)
[ ] Confirm conflict handling (server 409) reverts UI and shows error
[ ] Run stress test (≥100 rapid toggles) – no crash, final state matches parity
[ ] Validate localization (LTR & RTL) – icon mirrors correctly
[ ] Confirm backup/restore via ADB preserves favorites
[ ] Ensure multi‑user isolation (if applicable)
[ ] Verify behavior under battery optimization / Doze mode (if sync required)
[ ] Run SUSA autonomous exploration with at least three personas; review generated Appium tests for new findings
[ ] Check notification badge updates after favorite change (if applicable)
[ ] Review migration scripts for Room/SharedPreferences when schema changes
Mark each item as ✅ after you have executed the corresponding test on the current build.
---
Closing Takeaways
Favorites may seem like a trivial UI toggle, but they sit at the intersection of user interaction, data persistence, concurrency, accessibility, and sometimes network synchronization. A disciplined testing strategy therefore needs to span:
- Unit tests that guard the logic toggling the boolean flag and persisting it.
- Espresso/UIAutomator2 tests that verify immediate UI feedback, survival through configuration changes, process kills, and off‑screen scenarios.
- Manual exploratory steps that catch device‑specific quirks such as low storage, backup/restore, and multi‑user isolation.
- Automated tooling like SUSA that exercises the app through varied personas, surfacing edge cases (e.g., rapid taps from an impatient user, missing content descriptions for accessibility users) that deterministic scripts often overlook.
- Production monitoring (analytics, crash reporting, battery historian) to detect issues that only appear under real‑world constraints like I/O throttling, standby buckets, or sync conflicts.
By combining these layers, you gain confidence that the favorite feature behaves exactly as users expect—immediate, reliable, and accessible—across the full spectrum of Android devices and usage patterns. Treat favorites as a first‑class citizen in your test plan, and you’ll reduce silent bugs, improve user satisfaction, and free up engineering time for higher‑value work.
---
*End of guide.*
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