How to Test Filters And Sorting on Android (Complete Guide)
Filters and sorting are the UI mechanisms that let users turn a large data set into a manageable view. In e‑commerce apps a filter might narrow products by price, brand, or availability; in a news rea
Motivation: Why Filters and Sorting Deserve Dedicated Test Effort
Filters and sorting are the UI mechanisms that let users turn a large data set into a manageable view. In e‑commerce apps a filter might narrow products by price, brand, or availability; in a news reader sorting could arrange articles by recency, popularity, or relevance. When these controls misbehave the user experience degrades instantly—items disappear, wrong order appears, or the app crashes while trying to apply a predicate. Because the logic often lives in the presentation layer, it is easy to overlook during unit testing, yet the failure surface is wide: UI state, backend queries, caching, and accessibility services all interact. A dedicated test effort catches regressions that would otherwise slip into production and generate support tickets, poor ratings, or abandoned carts.
What Typically Breaks in Production
| Failure Category | Typical Symptom | Root Cause |
|---|---|---|
| UI state mismatch | Filter chip stays selected after clearing | ViewModel not observing LiveData correctly |
| Incorrect query generation | Sorting by price shows highest‑first despite ASC flag | Backend receives wrong query parameter |
| Data stale after filter | New items appear only after pull‑to‑refresh | Cache invalidation missed |
| Accessibility loss | TalkBack skips filter dropdown | Missing contentDescription or focus order |
| Security leak | Filter query logs user‑input to analytics | PII inadvertently sent |
| Performance drop | UI freezes for >2 s when applying many‑choice filter | Heavy work on main thread |
| Locale‑specific bug | Arabic layout shows filter icons mirrored incorrectly | RTL not handled in drawable resources |
| Race condition | Rapid tap on sort button yields duplicate network calls | No debouncing or mutex |
Understanding these patterns helps you build a test matrix that covers not only the happy path but also the failure modes that surface only under real‑world conditions.
Comprehensive Test Matrix
Below is a matrix you can copy into a test plan spreadsheet. Each row represents a test scenario; columns indicate the dimension you validate. Use ✅ for pass, ❌ for fail, and ⚠️ for flaky or conditional outcomes.
| # | Scenario | Happy Path | Error Path | Edge Case | Accessibility | Security/Privacy | Performance | Localization | Cross‑session State | Persona‑Driven |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | Apply single‑choice filter (e.g., Category = Electronics) | ✅ | – | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 2 | Clear all filters via “Reset” button | ✅ | – | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 3 | Apply multiple‑choice filter (brand + price range) | ✅ | – | – | ✅ | ✅ | ⚠️ (debounce) | ✅ | ✅ | ✅ |
| 4 | Select incompatible filters (e.g., Free + Paid) | – | ✅ (shows empty state) | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 5 | Apply filter when backend returns 500 | – | ✅ (error toast) | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 6 | Sort ascending/descending on a numeric column | ✅ | – | – | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 7 | Sort on a column with null values | ✅ | – | ✅ (nulls at bottom/top per spec) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 8 | Rapidly toggle sort order 10× in 2 s | – | – | ✅ (no duplicate requests) | ✅ | ✅ | ⚠️ (throttle) | ✅ | ✅ | ✅ |
| 9 | Apply filter while device rotates | ✅ | – | ✅ (state retained) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 10 | Filter with long‑text query (>100 chars) | ✅ | – | ✅ (truncation or validation) | ✅ | ✅ (no PII leak) | ✅ | ✅ | ✅ | ✅ |
| 11 | Filter using voice input (Accessibility Service) | ✅ | – | – | ✅ (TalkBack) | ✅ | ✅ | ✅ | ✅ | ✅ |
| 12 | Apply filter with TalkBack enabled, navigate via swipe | ✅ | – | – | ✅ (focus moves) | ✅ | ✅ | ✅ | ✅ | ✅ |
| 13 | Filter when app is in background (data sync) | ✅ | – | ✅ (UI reflects latest) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 14 | Apply filter after clearing app cache/reinstall | ✅ | – | ✅ (defaults restored) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 15 | Filter with remote‑config toggle (feature flag) | ✅ | – | ✅ (flag off hides UI) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 16 | Sorting with custom comparator (locale‑aware) | ✅ | – | ✅ (correct collation) | ✅ | ✅ | ✅ | ✅ (Arabic/Thai) | ✔️ | ✔️ |
| 17 | Power‑user: apply filter, then sort, then undo via back button | ✅ | – | ✅ (state stack) | ✅ | ✅ | ✅ | ✅ | ✔️ | ✔️ |
| 18 | Elderly persona: large‑font mode, filter touch target ≥48 dp | ✅ | – | – | ✅ (touch target) | ✅ | ✅ | ✅ | ✔️ | ✔️ |
| 19 | Impatient persona: tap filter, then immediately exit screen | – | – | ✅ (no crash) | ✅ | ✅ | ✅ | ✅ | ✔️ | ✔️ |
| 20 | Adversarial persona: inject SQL‑like string in filter text field | – | ✅ (sanitized, no error) | – | ✅ | ✅ (no injection) | ✅ | ✅ | ✔️ | ✔️ |
Feel free to add rows for specific product domains (e.g., date range filter for events, rating filter for reviews). The matrix gives you a concrete checklist that can be turned into automated test cases or manual exploratory sessions.
Manual Testing Approach – Step‑by‑Step
1. Prepare the Test Environment
- Device/emulator: Use a recent Android version (API 33+) and also test on at least one older API (e.g., 21) to catch deprecated API usage.
- Developer options: Enable “Show layout bounds”, “Strict mode”, and “Disable HW overlays” to overdraw issues.
- Logging: Connect via
adb logcat -v time | grep -E "Filter|Sort|ViewModel"to capture runtime traces.
2. Baseline Exploration
Launch the app and navigate to the screen containing the filter/sort UI.
- Note the initial state: which chips are selected, default sort order, visible item count.
- Take a screenshot for reference (
adb exec-out screencap -p > baseline.png).
3. Happy‑Path Validation
- Tap a single filter chip → verify it becomes highlighted.
- Observe the list update: item count changes, no stale items remain.
- Tap “Apply” (if present) → confirm network request matches expected query (use
adb logcator a proxy like Charles). - Change sort order → confirm list re‑renders with correct comparator.
4. Error‑Path Injection
- Simulate backend failures: use a tool like HttpCanary or Charles to return 500 or malformed JSON when the filter request is made.
- Verify that an error toast or empty state appears, and the UI does not crash.
5. Edge‑Case Execution
- Rapid interaction: Use a scripted monkey command (
adb shell monkey -p com.example.app -c android.intent.category.LAUNCHER 500 -throttle 200) focusing on the filter area to stress‑test debouncing. - Configuration change: Rotate device while a filter is applied; ensure the UI retains selections and the list does not flicker.
- Long text: Paste a 200‑character string into a text‑filter field; confirm the app either truncates, validates, or shows a clear error.
6. Accessibility Checks
- Turn on TalkBack and navigate to each filter chip using swipe gestures. Confirm each chip announces its label and state (selected/unselected).
- Use Accessibility Scanner (from Play Store) to detect missing contentDescriptions, low contrast, or touch targets <48 dp.
- Increase system font size to 200 % and verify layout does not truncate or overlap.
7. Security/Privacy Scan
- Enable Network Security Config logging to ensure no clear‑text HTTP requests contain filter parameters.
- If the app sends analytics, inspect the payload with a proxy to confirm that raw filter strings (especially PII like email or phone) are not included.
8. Performance Profiling
- Open Android Studio Profiler, select the UI thread, and record while applying a multi‑choice filter.
- Look for spikes >16 ms (jank) or sustained >200 ms work on the main thread. If present, move heavy filtering to a
CoroutineorWorkManager.
9. Localization & RTL
- Change device language to Arabic (or Hebrew) and verify that filter icons, dropdown arrows, and sort icons are mirrored correctly.
- Confirm that text alignment in filter chips respects RTL without clipping.
10. Cross‑Session State
- Apply a filter, then press home and relaunch the app after a few minutes. The filter should either persist (if designed) or reset to default per product spec.
- Use
adb shell dumpsys activity activities | grep mResumedActivityto verify the activity lifecycle.
11. Persona‑Driven Exploration
- Curious: Tap every chip, long‑press to see tooltips, try hidden gestures.
- Impatient: Tap filter, then immediately press back; ensure no crash or leak.
- Elderly: Use large font, test touch targets.
- Adversarial: Insert special characters (
' OR 1=1 --) into text filters; verify sanitization.
Document any deviation from the matrix as a bug, attaching logs, screenshots, and steps to reproduce.
Automated Approaches – Espresso, UIAutomator, and Appium
Espresso for Filter UI
@RunWith(AndroidJUnit4::class)
class FilterSortTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun `single choice filter updates list`() {
// Arrange – ensure we start from known state
onView(withId(R.id.btn_reset_filters)).perform(click())
// Act – tap the Electronics chip
onView(withText("Electronics")).perform(click())
// Assert – chip appears selected
onView(withText("Electronics"))
.check(matches(isSelected()))
// Assert – list reflects filter (e.g., at least one item shown)
onView(withId(R.id.recycler_view))
.check(matches(hasMinimumChildCount(1)))
}
@Test
fun `sort descending shows highest price first`() {
onView(withId(R.id.spinner_sort)).perform(click())
onView(withText("Price: High → Low")).perform(click())
onView(withId(R.id.recycler_view))
.check(matches(
atPosition(
0,
hasDescendant(withText(matches(`startsWith`("$"))))
)
))
}
}
*Key points*:
- Use
IdlingResourceif the app performs network requests after a filter; otherwise Espresso may assert before the list updates. - Parameterize the test with
@ParameterizedTest(via JUnit‑Params) to run the same logic for each filter category.
UIAutomator for Cross‑App Scenarios
If your filter launches a separate picker activity (e.g., date range), UIAutomator can interact across process boundaries:
public class FilterUiAutomatorTest {
private UiDevice device;
@Before
public void setUp() throws Exception {
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
}
@Test
public void dateRangeFilterAppliesCorrectly() throws UiObjectNotFoundException {
// Open filter drawer
device.findObject(new UiSelector().descriptionContains("Open filters")).click();
// Set start date
UiObject startDate = device.findObject(new UiSelector()
.resourceId("com.example.app:id/start_date"));
startDate.click();
device.findObject(new UiSelector().text("15")).click(); // day 15
// Set end date
UiObject endDate = device.findObject(new UiSelector()
.resourceId("com.example.app:id/end_date"));
endDate.click();
device.findObject(new UiSelector().text("22")).click(); // day 22
// Confirm
device.findObject(new UiSelector().text("Apply")).click();
// Verify that at least one item matches the range
UiObject list = device.findObject(new UiSelector()
.resourceId("com.example.app:id/item_list"));
assertTrue(list.getChildCount() > 0);
}
}
Appium for Hybrid or WebViews
When part of the filter UI lives in a WebView (common for hybrid apps), Appium lets you switch contexts:
from appium import webdriver
from appium.webdriver.common.mobileby import MobileBy
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
caps = {
"platformName": "Android",
"deviceName": "Pixel_4_API_33",
"app": "/path/to/app.apk",
"automationName": "UiAutomator2"
}
driver = webdriver.Remote("http://localhost:4723/wd/hub", caps)
# Switch to native context to open filter
driver.find_element(MobileBy.ACCESSIBILITY_ID, "Open filters").click()
# Switch to WebView
WebDriverWait(driver, 10).until(lambda d: len(d.contexts) > 1)
driver.context = driver.contexts[1] # usually "WEBVIEW_com.example.app"
# Interact with HTML filter
driver.find_element(MobileBy.ID, "category-select").send_keys("Electronics")
driver.find_element(MobileBy.ID, "apply-btn").click()
# Switch back and validate list
driver.context = driver.contexts[0]
assert driver.find_element(MobileBy.ID, "item_count").text != "0"
driver.quit()
Leveraging SUSA for Autonomous, Persona‑Driven Discovery
SUSA can explore the filter/sort screen without any test code. After installing the agent:
pip install susatest-agent
susatest explore --apk path/to/app.apk --personas curious impatient elderly adversarial
The agent will:
- Launch the app, locate filter chips via UI hierarchy, and try every combination it can infer (single‑choice, multi‑choice, range).
- Simulate each persona’s timing: the *impatient* persona taps and backs out quickly, the *elderly* persona uses long presses and waits for animations, the *adversarial* persona injects strings like
or SQL snippets. - Capture crashes, ANRs, accessibility warnings (via Android Accessibility Test Framework), and network anomalies.
- After the run, it outputs a set of regression scripts in Appium (Android) and Playwright (Web) that you can commit to your CI pipeline.
Because SUSA does not rely on pre‑written locators, it often finds bugs that static scripts miss—for example, a filter chip that becomes invisible only when a specific remote‑config flag is enabled, or a sort spinner that loses its popup anchor after a configuration change.
Tooling Specific to Android
| Tool | Purpose | Typical Use in Filter/Sort Testing |
|---|---|---|
| Layout Inspector (Android Studio) | View live view hierarchy, properties, and constraints | Verify that filter chips have correct contentDescription, enabled, and visibility states after each interaction |
| StrictMode | Detect disk/network access on main thread | Enable to catch heavy filtering logic that blocks UI |
| LeakCanary | Memory leak detection | Ensure that ViewModels or adapters tied to filtered lists are cleared when navigating away |
| Profiler (CPU, Memory, Network) | Identify performance spikes and excessive allocations | Profile a multi‑choice filter to confirm work is offloaded to a coroutine or RxJava thread |
Android Accessibility Test Framework (via androidx.test:core) | Programmatic accessibility checks | Assert that each filter node has a non‑null contentDescription and appropriate role |
| Firebase Test Lab | Run instrumentation tests on a matrix of real devices | Validate filter behavior across different screen densities, OEM skins, and Android versions |
| Network profiling tools (Charles, HttpCanary, Stetho) | Inspect request/response payloads | Confirm that filter query parameters are correctly formed and that no sensitive data leaks |
Integrate these tools into your local dev workflow and CI pipelines; many have Gradle plugins that fail the build on detected violations.
Edge Cases That Only Show Up in Production
1. Dynamic Remote Configs
A/B testing frameworks may enable a new filter UI only for a fraction of users. If your test suite runs against a static build, you never see the new branch. Mitigation:
- Fetch the latest config from your feature‑flag service at test start (
Firebase Remote Config,LaunchDarkly). - Parameterize tests with the flag value and run both ON and OFF matrices.
2. Server‑Side Schema Drift
Backend may add a new field (e.g., isPremium) that the client uses for sorting but forgets to update the UI label. The app may crash when trying to access a missing key in JSON. Mitigation:
- Use schema validation (e.g.,
MoshiwithJsonAdapter.FAIL_ON_UNKNOWN_MEMBERS) in unit tests. - In UI tests, assert that sorting options displayed correspond to actual fields present in the latest API response (contract test).
3. Intermittent Network Conditions
A filter request may succeed on Wi‑Fi but fail under LTE latency, exposing a missing error‑state handler. Mitigation:
- Use the Network Emulator in Android Studio to simulate 3G, LTE, or packet loss while running Espresso tests.
- Assert that a retry mechanism or offline fallback appears.
4. Data‑Change Races
Imagine a user applies a “price < $20” filter while a background sync adds a new $15 item. If the adapter does not call notifyItemInserted() on the filtered subset, the new item may never appear. Mitigation:
- Use DiffUtil with a payload that respects the current filter predicate.
- Write a test that inserts a new item via a mock repository while the filter UI is active and asserts the list updates.
5. Accessibility Service Interference
Some devices ship with third‑party accessibility services that overlay or modify touch events. This can cause a filter chip to receive a double‑tap or be ignored. Mitigation:
- Test with TalkBack, Switch Access, and a popular third‑party service (e.g., Voice Access) enabled.
- Ensure that your UI does not rely on
android:clickable="true"alone; preferandroid:focusableand properonClickhandling.
6. Locale‑Specific Collation Bugs
Sorting by name in Thai or Japanese may produce unexpected order if the app uses String.CASE_INSENSITIVE_ORDER instead of Collator. Mitigation:
- Unit test the comparator with
java.text.Collator.getInstance(Locale). - In UI tests, verify that the displayed list matches the expected collated order for each supported locale.
7. Battery‑Optimization Interference
On some OEMs, aggressive battery optimization may suspend background workers that refresh filtered data after a network change. Mitigation:
- Test with Battery Historian to see if your app is placed in a restricted bucket.
- Request a
WHITE_LISTor useWorkManagerwithsetExpedited(true)for critical refreshes.
8. Multi‑Window and Free‑Form Mode
When the app runs in split‑screen, the filter drawer may be clipped or the recycler view may not receive correct size updates. Mitigation:
- Run UI tests in multi‑window mode using
adb shell am start -W -n com.example.app/.MainActivity --ei windowingMode 2. - Assert that layout parameters adjust (
match_parentvs fixed dimensions).
Document each of these edge cases in your test plan; treat them as “production‑only” rows in the matrix and add specific automated checks where feasible.
Accessibility and Localization Checklist
- Touch Targets: Minimum 48 dp; use
android:minWidth/android:minHeightor padding. - Content Description: Every interactive element (chip, spinner, button) must have a meaningful
contentDescriptionthat states its function and current state (e.g., “Filter: Electronics, selected”). - Focus Order: Logical left‑to‑right, top‑to‑bottom flow; verify with TalkBack linear navigation.
- Contrast: WCAG AA minimum 4.5:1 for normal text, 3:1 for large text. Use the Accessibility Scanner or
androidx.core.view.ViewCompat.getBackgroundTintList. - Scalable Text: Ensure
spunits for text; test with font scale 1.2–2.0. - RTL Layout: Use
android:start/android:endinstead ofleft/right; verify mirroring of icons and drawables viaLayoutDirection. - Locale‑Specific Formats: Dates, numbers, and currencies should be formatted with
java.text.NumberFormatandDateFormatfor the current locale. - Voice Input: Test with
SpeechRecognizer; ensure hint text is spoken and results are correctly parsed.
Put these items in a shared accessibility test suite that runs on every PR.
Security and Privacy Implications
Filters often expose user intent (e.g., “show only free items”, “hide adult content”). If this intent is logged or transmitted insecurely, it can reveal sensitive preferences.
- Network Encryption: Enforce TLS 1.2+ for all API calls; use
network_security_config.xmlto block cleartext. - Analytics Sanitization: Strip or hash filter values before sending to analytics endpoints. Prefer event names like
filter_appliedwith only category IDs, not raw strings. - Input Validation: Treat filter text as untrusted; guard against injection (SQL, NoSQL, XSS if rendered in a WebView). Use parameterized queries or ORM escaping.
- Permission Boundaries: If a filter accesses contacts, location, or media, request runtime permission only when the filter is actually applied, not on screen load.
- Data Minimization: Request only the fields needed for the filtered view from the backend; avoid downloading full objects then discarding most of them client‑side.
Automated security scans (MobSF, OWASP ZAP) should include the filter endpoints as part of the API surface.
Concise Checklist for Daily Use
| ✅ Item | How to Verify |
|---|---|
| Filter chip state updates | Tap chip → isSelected() true; list reflects change |
| Reset button clears all | After reset, all chips unselected, list shows original count |
| Multi‑choice filter works | Select ≥2 chips → list shows intersection |
| Incompatible filters handled | Selecting mutually exclusive options shows empty state or error |
| Sort order toggles correctly | Click sort → list order reverses; repeat → original order |
| Null values placed per spec | Add null‑valued item → appears at start/end per requirement |
| Accessibility labels present | TalkBack reads each chip’s label and state |
| Touch target ≥48 dp | Use Layout Inspector → verify bounds |
| No main‑thread jank >16 ms | Profile UI thread while applying filter |
| Network calls use TLS | Charles/HTTPS proxy shows only https:// |
| No PII in analytics payload | Proxy shows only anonymized IDs |
| Locale formats correct | Switch device language → dates/numbers adapt |
| RTL layout mirrors | Switch to Arabic → icons flow right‑to‑left |
| State survives rotation | Rotate device while filter applied → selections persist |
| Background sync updates list | Add item via mock repo while filter on → list updates |
| No crash on rapid taps | Monkey 500 events on filter area → app stays alive |
| Error state shown on 500 | Mock server error → toast or empty state appears |
| Persists across app kill (if spec) | Close app, relaunch → filter retained or reset per design |
| Works in multi‑window | Launch in split‑screen → filter UI usable, not clipped |
| Passes accessibility scanner | Run scanner → zero high‑severity issues |
| Passes strict mode | Enable StrictMode → no disk/network on main thread |
| No memory leak on navigation | LeakCanary reports no leaks after leaving filter screen |
| Works with third‑party accessibility | Enable Voice Access → filter operable |
Run this checklist manually for smoke testing, and automate the items that have deterministic assertions.
Final Takeaways
Filters and sorting are seemingly simple UI components, yet they sit at the intersection of user intent, data pipelines, accessibility, and performance. A disciplined testing strategy—starting with a well‑defined matrix, exercising both happy and error paths, validating accessibility and security, and stressing the system with edge‑case scenarios unique to production—prevents the class of bugs that erode trust and drive users away.
Leverage Android’s rich tooling (Layout Inspector, Profiler, StrictMode, Accessibility Test Framework) to catch regressions early in the development cycle. Complement automated Espresso/UIAutomator tests with autonomous, persona‑driven explorers like SUSA, which can surface hidden interaction patterns that static scripts never consider.
When you treat filter and sort testing as a first‑class concern—backed by concrete matrices, reproducible steps, and continuous verification—you ship apps where users can reliably narrow down and order content, no matter their device, language, or interaction style. This diligence translates into higher engagement, fewer support tickets, and a reputation for quality that compounds over every release.
---
*Prepared for engineers who want a repeatable, battle‑tested approach to validating one of the most used—and most fragile—parts of any Android app.*
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