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

February 04, 2026 · 15 min read · How-To Guides

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 LayerTypical APIsProsCons
SharedPreferencesgetSharedPreferences(), edit().putBoolean()Simple, fast, private to the appNo built‑in sync, limited to primitive types
SQLite / Room@Entity, DAO methodsStructured queries, migration supportRequires schema management, more boilerplate
Remote backend (REST/GraphQL)Retrofit, Coroutines, LiveDataEnables cross‑device sync, server‑side validationNetwork 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:

  1. User interaction (click, long press, swipe) →
  2. ViewModel / Presenter toggles a boolean flag →
  3. Flag persisted (local or remote) →
  4. 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.

#ScenarioDescriptionManualUnit TestUI Test (Espresso/UIAutomator)Expected Result
1Happy path toggleUser taps favorite icon on an item that is not favored; icon fills, badge increments.✔ (ViewModel toggle)✔ (click + assert drawable)State persisted, UI updated instantly
2UnfavoriteUser taps filled icon; icon reverts to outline, badge decrements.State cleared, UI updated
3Rapid double‑tapTwo taps within 200 ms (debounce test).✔ (use performClick() twice with Thread.sleep(10))Only one state change occurs
4Toggle 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
5Configuration changeRotate device or change language while favorite is pending.✔ (ViewModel survives)✔ (rotate, assert UI)No loss of state, UI consistent
6Process kill & restoreSwipe app from recent, relaunch; favorite state must be restored.✔ (Repository load)✔ (kill via adb shell am force-stop, relaunch, assert)Persisted state survives
7Low storage conditionDevice 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)
8Network 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
9Conflict 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
10Accessibility – content descriptionFavorite icon announces state correctly for TalkBack.✔ (use accessibilityLiveRegionTalkBack reads “favorite, selected” or “not favorite”
11Touch target sizeMinimum 48 dp touch area around icon.✔ (use UiAutomator to get bounds)Hit‑test passes
12Color contrastIcon meets WCAG AA (≥ 4.5:1) against background.✔ (use Android Studio’s Accessibility Scanner)Contrast ratio compliant
13Data leakage – logsNo favorite identifiers appear in Logcat at VERBOSE level.— (inspect adb logcat)No PII in logs
14Permission misuseApp does not request unnecessary permissions (e.g., READ_CONTACTS) just to store favorites.— (check manifest)No extraneous permissions
15Backup/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
16Multi‑user / profileFavorite state isolated between work profile and personal profile.— (switch user, test)No cross‑profile leakage
17Battery optimization whitelistApp continues to sync favorites in Doze mode if required.— (use adb shell dumpsys battery unplug)Sync occurs as expected
18Stress – rapid toggling100 rapid toggles via automation; verify final state matches parity.✔ (loop 100 clicks)No crash, final state correct
19Localization – RTL layoutFavorite icon mirrors correctly in right‑to‑left languages.✔ (set locale to ar-EG, assert layout)Icon positioned correctly
20Dark modeIcon retains proper contrast in night mode.✔ (enable night mode, assert)Visual consistency

*Notes*:

---

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:

  1. Preparation
  1. Baseline Navigation
  1. Happy Path Toggle
  1. Unfavorite
  1. Rapid Interaction
  1. Scroll‑and‑Favorite
  1. Configuration Change
  1. Process Kill
  1. Low Storage Simulation
  1. Network Loss & Recovery
  1. Accessibility Check
  1. Permission & Log Hygiene
  1. Backup/Restore
  1. Multi‑User Test (if device supports)
  1. Battery Optimization
  1. Stress Test

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:

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 ModeRoot CauseHow to Observe in ProdMitigation
Delayed persistence due to I/O throttlingAndroid’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 editUser 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 shadeA 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 updateMigration 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 pollingApp 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” restrictionsApp 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 clipboardDebug 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 modeLayout 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:

  1. Unit tests that guard the logic toggling the boolean flag and persisting it.
  2. Espresso/UIAutomator2 tests that verify immediate UI feedback, survival through configuration changes, process kills, and off‑screen scenarios.
  3. Manual exploratory steps that catch device‑specific quirks such as low storage, backup/restore, and multi‑user isolation.
  4. 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.
  5. 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