How to Test Analytics Dashboard on Android (Complete Guide)
Analytics dashboards are the visual layer that turns raw event data into actionable insights for product, marketing, and executive teams. On Android, these dashboards often sit inside a settings or “I
Why Testing Analytics Dashboards Matters
Analytics dashboards are the visual layer that turns raw event data into actionable insights for product, marketing, and executive teams. On Android, these dashboards often sit inside a settings or “Insights” screen, consume data from a local SQLite room or a remote REST endpoint, and render charts using libraries such as MPAndroidChart, HelloCharts, or custom Canvas drawing. When the dashboard fails, stakeholders receive stale or misleading metrics, which can trigger wrong product decisions, wasted ad spend, or compliance violations.
A broken dashboard can manifest in ways that are not caught by functional UI tests:
- Data latency – the chart shows yesterday’s numbers while the backend already updated.
- Incorrect aggregation – sum vs. average mismatch leads to inflated conversion rates.
- Rendering glitches – overflowing labels, clipped axes, or missing legends on specific screen densities.
- State corruption – rotating the device clears filters or resets date pickers.
- Security leakage – exposing raw event payloads in tooltips or logs.
Because the dashboard is a consumer of analytics pipelines, testing it validates the end‑to‑end flow from instrumentation → storage → presentation. It also surfaces bugs in the underlying analytics SDK that would otherwise stay hidden until a data‑quality incident appears in production.
Core Components of an Android Analytics Dashboard
Before designing tests, identify the moving parts that belong to the dashboard module:
| Component | Typical Implementation | Test Focus |
|---|---|---|
| Data source | Room DAO, Firebase Firestore listener, or custom REST client with Retrofit | Correct query, caching, and refresh semantics |
| ViewModel | Holds LiveData/Flow of chart data, handles loading/error states | State transitions, thread‑safety, config‑change survival |
| UI layer | Fragment or Compose screen with RecyclerView, MPAndroidChart, or custom View | Layout correctness, accessibility labels, touch targets |
| Chart library | MPAndroidChart (LineChart, BarChart, PieChart) or HelloCharts | Data‑to‑pixel mapping, axis formatting, gesture handling |
| Filters / controls | DatePicker, Spinner, Switch, ChipGroup | Persistence of selections, reset behavior, edge‑case values |
| Export / share | Intent.ACTION_SEND, FileProvider, or third‑party SDK | MIME type, file URI permissions, content‑uri correctness |
| Error handling | Toast, Snackbar, or error‑state placeholder view | Graceful degradation, retry mechanisms, user‑friendly messaging |
Understanding these boundaries lets you isolate failures: a chart that never updates may be a ViewModel LiveData issue, while a chart that draws incorrectly despite correct data points to the chart library or custom drawing code.
Comprehensive Test Matrix
The following table maps test categories to concrete scenarios, expected outcomes, and the Android‑specific mechanisms you can use to verify them. Use it as a checklist when building manual or automated test suites.
| Category | Scenario ID | Description | Expected Result | Verification Method (Android) |
|---|---|---|---|---|
| Happy Path | HP1 | Launch dashboard from main nav, default date range (last 7 days) loads without error | Chart displays data points, axes labeled, legend visible | Espresso/Compose test: assert chart visibility, check data‑point count via ViewModel LiveData |
| HP2 | User selects a custom date range (e.g., Jan 1‑Jan 15) via DatePicker | Chart updates to show only selected range data | Trigger date picker, set dates, assert ViewModel receives new range, chart redraws | |
| HP3 | Switching between chart types (line ↔ bar) via tab layout | Chart type changes, data remains consistent | Click tab, verify chart class changes, data values unchanged | |
| Error Paths | EP1 | Backend returns 500 error; dashboard shows error placeholder and retry button | Error view visible, retry triggers new request | Mock server with WireMock returning 500, assert error UI, click retry, verify success path |
| EP2 | Empty data set (no events for selected range) → empty state message | Placeholder text “No data” displayed, no chart rendered | Insert zero rows into Room, assert empty‑state visibility | |
| EP3 | Corrupt cached data (malformed JSON) → fallback to network | Dashboard discards cache, fetches fresh data, shows loading spinner | Corrupt Room entry, observe loading indicator, then success UI | |
| Edge Cases | EC1 | Screen rotation while date picker is open | Date picker retains selected values, chart not reset | Rotate device, verify DatePicker state, chart unchanged |
| EC2 | Font scale set to 200% (large text) | All labels, axis titles, tooltips scale without clipping | Change android:fontScale via adb, assert no overlapping text via UIAutomator | |
| EC3 | TalkBack enabled; user navigates via swipe | Each chart element (axis, data point, legend) announces appropriate content‑description | Enable TalkBack, use UiDevice to swipe, capture spoken feedback via AccessibilityEvent | |
| EC4 | Device in battery‑saver mode; background throttling | Dashboard respects JobScheduler constraints, does not start unnecessary sync | Enable battery saver, use adb shell dumpsys jobscheduler, verify no premature jobs | |
| EC5 | Multi‑window (split‑screen) mode; dashboard occupies top half | Layout adapts, chart remains interactive, no overlapping with system UI | Launch in split‑screen via adb shell am start -W -n com.example/.DashboardActivity --ei split_screen 1, test touch | |
| Accessibility | AC1 | Minimum touch target size ≥48 dp for all interactive elements | DatePicker, filter chips, export button meet size rule | Use Android Studio Layout Inspector or uiautomatorviewer to measure bounds |
| AC2 | Color contrast ratio ≥4.5:1 for text vs. background | Axis labels, legend text pass contrast check | Run axe-android or manual contrast calculation with screenshots | |
| AC3 | Screen reader announces state changes (loading → data) | Live region updates trigger announcement | Enable TalkBack, filter for TYPE_VIEW_TEXT_CHANGED events | |
| Security & Privacy | SP1 | No raw event JSON appears in tooltips or clipboard | Tooltip shows aggregated values only | Long‑press tooltip, check clipboard content via adb shell clip get |
| SP2 | Export file stored in app‑specific directory, not external storage | FileProvider URI granted only to chosen share target | Initiate share, verify content:// URI, confirm no file:// scheme | |
| SP3 | Network calls use TLS 1.2+ and certificate pinning (if enabled) | No clear‑text HTTP traffic observed | Use adb shell tcpdump or Stetho to inspect packets | |
| Performance | PF1 | Initial load ≤2 s on median device (Snapdragon 765G) | Stopwatch from launch to first chart draw | Use adb shell am start -W and measure TotalTime |
| PF2 | Chart redraw after filter change ≤500 ms | Measure frame drops via adb shell gfxinfo | Reset stats, trigger filter, collect jank frames | |
| PF3 | Memory leak absent after 10 navigation cycles | Heap size stable after repeated open/close | Use Android Studio Profiler, force GC, observe retained size |
How to Read the Matrix
- Category groups similar risk areas.
- Scenario ID gives a stable reference for test‑case management tools (e.g., TestRail, Zephyr).
- Verification Method points to the Android‑specific technique you should adopt; you can replace Espresso with Compose test or UIAutomator as needed.
Populate your test‑management system with these IDs, then map each to either a manual test script or an automated test class.
Manual Testing Workflow
Even when you invest heavily in automation, a disciplined manual pass catches context‑sensitive issues that scripts overlook. Follow this step‑by‑step routine for each build candidate.
- Environment preparation
- Install the debug APK on a representative device (e.g., Pixel 6 API 33) and a low‑end device (e.g., Nokia 2.2 API 28).
- Clear app data (
adb shell pm clear com.example.app) to start from a clean slate. - Enable Developer options → Show layout bounds and Show CPU usage for visual checks.
- Baseline happy‑path walkthrough
- Launch the app, navigate to the Analytics Dashboard.
- Verify the loading spinner appears for ≤1 s, then the chart renders with expected data points (you can cross‑check against a known‑good CSV export from the backend).
- Confirm that axis labels, legend, and tooltip appear correctly.
- Date‑range manipulation
- Open the date picker, select a range that spans a month boundary, and confirm the chart updates instantly.
- Rotate the device while the picker is open; ensure the selected dates survive rotation.
- Try an invalid range (end date before start date) – the app should either swap dates or show an error toast.
- Error injection
- Use a local proxy (e.g.,
mitmproxy) to return 500 or malformed JSON for the analytics endpoint. - Observe that the dashboard shows an error view with a retry button.
- Tap retry after fixing the proxy to return 200; ensure the chart recovers without a full app restart.
- Accessibility audit
- Turn on TalkBack, navigate through every interactive element using swipe gestures.
- Listen for meaningful descriptions (e.g., “Line chart, data point 3, value 12.4 %”).
- Switch to font scale 200% in Settings → Accessibility → Font size; verify no text is truncated.
- Security & privacy sniffing
- Long‑press a tooltip; open the clipboard (
adb shell clip get) and confirm it does not contain raw JSON. - Trigger the export/share flow; use
adb shell cmd content query --uri content://com.example.app.provider/exportto verify the URI scheme iscontent://.
- Performance spot‑check
- Run
adb shell am start -W -n com.example.app/.DashboardActivityand note theTotalTimereported. - Open the GPU inspector (
adb shell gfxinfo com.example.app reset) then perform a filter change and runadb shell gfxinfo com.example.app framestatsto capture jank.
- Clean‑up
- Force‑stop the app, clear cache, and repeat the flow on the low‑end device to ensure resource‑constrained behavior remains acceptable.
Document any deviations in a spreadsheet linked to the test‑matrix IDs. This manual log becomes the baseline for automated test expectations.
Automated Testing with Android Frameworks
Automated checks give you regression safety and enable CI gating. Below are the recommended layers and sample implementations.
Unit & ViewModel Tests (JUnit + MockK)
Test the ViewModel logic in isolation:
@ExperimentalCoroutinesApi
class DashboardViewModelTest {
private val testDispatcher = UnconfinedTestDispatcher()
private lateinit var viewModel: DashboardViewModel
private lateinit var repo: MockAnalyticsRepository
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
repo = mockk(relaxed = true)
viewModel = DashboardViewModel(repo)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loads data when repository returns non‑empty list`() = runTest {
val sample = listOf(
ChartEntry(day = "Mon", value = 10f),
ChartEntry(day = "Tue", value = 15f)
)
coEvery { repo.fetchChartData(any(), any()) } returns sample
viewModel.loadData(start = Day.Monday, end = Day.Tuesday)
assertEquals(sample, viewModel.chartData.getOrThrow())
assertFalse(viewModel.isLoading.getOrThrow())
}
@Test
fun `shows error state on repository failure`() = runTest {
coEvery { repo.fetchChartData(any(), any()) } throws IOException("network")
viewModel.loadData(start = Day.Monday, end = Day.Tuesday)
assertTrue(viewModel.hasError.getOrThrow())
assertEquals("Unable to load data", viewModel.errorMessage.getOrThrow())
}
}
*Why this matters*: The ViewModel decides when to show loading, error, or data states. Unit tests guarantee those transitions are deterministic, independent of UI timing.
UI Tests with Espresso (View‑based)
For fragments that still use the classic View system:
@LargeTest
class DashboardEspressoTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun happyPath_loadsChartAndAllowsDateChange() {
// Navigate to dashboard
onView(withId(R.id.nav_analytics)).perform(click())
// Wait for loading spinner to disappear
onView(withId(R.id.progress_bar)).check(matches(not(isDisplayed())))
// Verify chart is drawn (using a custom matcher)
onView(withId(R.id.line_chart)).check(matches(ChartIsDrawn()))
// Open date picker and set a custom range
onView(withId(R.id.btn_date_picker)).perform(click())
onView(withText("OK")).perform(click()) // assumes preset dates
// Chart should update – we assert a specific data‑point label changed
onView(withId(R.id.line_chart)).check(matches(ChartHasValueAtIndex(0, 12.3f)))
}
// Custom matcher using MPAndroidChart's API via a test-only accessor
private fun ChartIsDrawn(): Matcher<View> = object : TypeSafeMatcher<View>() {
override fun matchesSafely(item: View): Boolean {
return item is ComposeView && item.chartData?.isNotEmpty() == true
}
override fun describeTo(description: Description) {
description.appendText("chart with at least one entry")
}
}
private fun ChartHasValueAtIndex(index: Int, expected: Float): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun matchesSafely(item: View): Boolean {
val chart = (item as LineChart)
val entry = chart.data?.getDataSetByIndex(0)?.getEntryForIndex(index)
return entry?.y == expected
}
override fun describeTo(description: Description) {
description.appendText("chart entry at $index equals $expected")
}
}
}
}
*Key points*:
- Use
IdlingResourceto wait for background Room queries or Retrofit calls. - Custom matchers let you assert chart internals without exposing them in production code (keep the accessor
internaland annotate with@VisibleForTesting).
UI Tests with Jetpack Compose
If the dashboard is written in Compose, the testing approach changes slightly:
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
@Test
fun `compose dashboard shows data and reacts to date filter`() {
// Navigate
composeTestRule.onNodeWithText("Analytics").performClick()
// Wait for loading indicator to disappear
composeTestRule.onNodeWithTag("Loading").assertNotExists()
// Assert chart contains expected number of data points
composeTestRule.onNodeWithTag("LineChart")
.assertExists()
.assert(hasChartDataSize(7)) // custom semantics property
// Open date picker
composeTestRule.onNodeWithText("Select Range").performClick()
composeTestRule.onNodeWithText("Start").performClick()
composeTestRule.onNodeWithText("15").performClick() // day
composeTestRule.onNodeWithText("OK").performClick()
// Verify chart updated
composeTestRule.onNodeWithTag("LineChart")
.assert(hasChartDataSize(10to be
}
Add a custom semantics property** in the chart composable ) {
// new range yields fewer points
expected = 3
))
}
// Custom semantics matcher
fun SemanticsMatcher.hasChartDataSize(expected: Int): Matcher
return object : Matcher
override fun matchesSafely(item: Assertion): Boolean {
val chart = (item as ComposeNode).getOrNull
return chart?.dataPoints?.size == expected
}
override fun describeMismatch(item: Assertion): String {
return "Chart data size ${item as ComposeNode}.getOrNull
}
}
}
*Notes*:
- Expose a `ChartState` object via `remember { mutableStateOf(...) }` and give it a test tag (`testTag = "ChartState"`).
- Use `createAndroidComposeRule` to launch the activity in a test‑isolated window.
### Instrumented Tests for Permissions & File‑URI
Validate the export flow with `ActivityScenario` and `Intent`:
@Test
fun export creates content uri and grants permission() {
val scenario = ActivityScenario.launch(DashboardActivity::class.java)
scenario.onActivity { activity ->
// Click share button
val shareBtn = activity.findViewById
shareBtn.performClick()
}
// Wait for the chooser to appear
val intent = Intent(Intent.ACTION_SEND)
val resolved = InstrumentationRegistry.getInstrumentation()
.targetContext
.packageManager
.resolveActivity(intent, 0)
assertNotNull(resolved)
// Grab the Uri from the clipboard or from a mock ContentProvider
val clip = getClipContent()
assertTrue(clip.startsWith("content://"))
}
Helper to read clipboard:
private fun getClipContent(): String {
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
return clipboard.getItemAt(0)?.text?.toString() ?: ""
}
### CI Integration
Add the following to your GitHub Actions workflow (adjust for Bitrise, GitLab, etc.):
name: Android CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'
- name: Cache Gradle
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run unit tests
run: ./gradlew testDebugUnitTest --no-daemon
- name: Run instrumented tests on emulator
run: |
./gradlew connectedAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.clearPackageData=true \
--no-daemon
env:
ADB_INSTALL_TIMEOUT: 120
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: app/build/reports/tests/
This workflow runs unit tests on the JVM, then spins up an Android emulator (via the `connectedAndroidTest` task) to execute Espresso/Compose UI tests. Adjust the emulator image (`-Pandroid.testInstrumentationRunnerArguments...`) to target API 33 for the latest behavior.
## Edge Cases that Surface Only in Production
Automated suites excel at regression, but certain bugs only appear under real‑world conditions. Below are the most common production‑only pitfalls for analytics dashboards and how to surface them deliberately.
| Issue | Why it hides in tests | Detection technique |
|-------|----------------------|---------------------|
| **Time‑zone drift** | Test devices often run in UTC; users in GMT+5:30 see date boundaries shift, causing off‑by‑one errors in daily aggregates. | Set device time zone via `adb shell setpersist.persist.sys.timezone Asia/Kolkata` and run the date‑picker matrix. |
| **Locale‑specific number formatting** | Charts may hard‑code a decimal point (“.”) while some locales use a comma (”,”), leading to parsing exceptions when formatting tooltip values. | Change locale: `adb shell setprop persist.sys.language fr && adb shell setprop persist.sys.country FR && adb reboot`. Verify tooltip text with TalkBack. |
| **Low‑memory killer** | Emulators have generous heap; on a device with 2 GB RAM, the system may kill the background SyncService, leaving stale cache. | Use `adb shell am kill com.example.app` after launching the dashboard, then restore and observe whether the ViewModel reloads from network. |
| **Network type throttling** | Tests usually run on unthrottled Wi‑Fi; on 2G or metered connections, the app may incorrectly show a loading spinner forever if it fails to honor `ConnectivityManager.isActiveNetworkMetered()`. | Simulate slow network: `adb shell tc qdisc add dev wlan0 root netem delay 500ms loss 5%` and then test retry logic. |
| **Multiple account profiles** | Enterprise devices with work profiles may isolate the app’s storage, causing the dashboard to read from the wrong profile’s Room database. | Enable a work profile via `adb shell am set-profile-owner com.example.app/.DeviceAdminReceiver` and check data isolation. |
| **Overlay or screen‑filter apps** | Apps like Twilight or blue‑light filters draw over the UI, potentially blocking touch events on the date picker. | Install an overlay app, grant `SYSTEM_ALERT_WINDOW`, then attempt to interact with the dashboard; ensure touch events still reach the chart. |
| **Battery‑optimization whitelist removal** | If the user disables battery optimization for the app, the periodic sync may run more frequently, causing duplicate entries and chart spikes. | Toggle `adb shell cmd deviceidle tempwhitelist + com.example.app` and monitor the sync frequency via `adb shell dumpsys jobscheduler`. |
| **Android 13+ runtime permission changes** | Starting Android 13, posting notifications requires `POST_NOTIFICATIONS`. If the dashboard uses a notification to signal new data, missing the permission leads to silent failures on newer OS versions. | Test on an API 33 emulator with the permission revoked (`adb shell pm revoke com.example.app android.permission.POST_NOTIFICATIONS`) and verify that the dashboard falls back to in‑app indicator. |
**Production‑only.
### Proactive Chaos Testing
Introduce a lightweight chaos runner in your CI or local dev environment that randomly applies one of the above conditions before each test iteration. Example Bash snippet:
#!/usr/bin/env bash
# chaos.sh – apply a random condition before running tests
CONDITIONS=(
"setprop persist.sys.timezone America/New_York"
"setprop persist.sys.language es && setprop persist.sys.country ES"
"tc qdisc add dev wlan0 root netem delay 300ms loss 10%"
"am kill com.example.app"
"cmd appops set com.example.app SYSTEM_ALERT_WINDOW ignore"
)
CHOICE=${CONDITIONS[$RANDOM % ${#CONDITIONS[@]}]}
echo "Applying chaos: $CHOICE"
adb shell $CHOICE
# run your test suite
./gradlew connectedAndroidTest
# cleanup (optional)
adb shell tc qdisc del dev wlan0 root netem
Running this loop a few times per commit surface flaky tests that depend on stable environment assumptions.
## Accessibility, Privacy & Security Checks
Beyond the matrix items, embed these lightweight validations into every test run.
### Accessibility Test Suite (using `androidx.test.espresso.accessibility`)
Add this rule to every UI test class:
@get:Rule
val accessibilityRule = AccessibilityChecks.Enable()
It runs automated checks for contrast, touch target size, and content description on each view hierarchy after every UI action. Failures appear as `PerformException` with details you can fix immediately.
### Privacy‑Preserving Test Hooks
Create a `@VisibleForTesting` flag that forces the dashboard to use a mock analytics endpoint returning synthetic data. In your test source set:
// src/androidTest/java/com/example/app/util/TestConfig.kt
object TestConfig {
const val USE_MOCK_ENDPOINT = true
}
Then in your repository implementation:
if (TestConfig.USE_MOCK_ENDPOINT) {
return mockChartData()
} else {
return remoteDataSource.fetch()
}
This guarantees no real user data leaves the device during CI runs.
### Security Scan Integration
Add the `MobSF` (Mobile Security Framework) or `OWASP Dependency‑Check` step to your CI pipeline to catch:
- Hard‑coded API keys in `strings.xml` or `BuildConfig`.
- Usage of `cleartextTrafficPermitted="true"` in `AndroidManifest.xml`.
- Out‑of‑date charting libraries with known CVEs (e.g., MPAndroidChart < 3.1.0).
A simple GitHub Actions step:
- name: Run MobSF scan
uses: mobsf/mobsf-action@v0.1
with:
apk: app/build/outputs/apk/debug/app-debug.apk
format: sarif
Upload the SARIF result to GitHub Security tab for alerting.
## Persona‑Driven Exploration with SUSA
Scripted tests verify known paths, but real users behave in unpredictable ways. An autonomous, persona‑driven explorer can exercise the dashboard under conditions that no manual tester would think to script.
### How SUSA Works (brief)
SUSA uploads your APK (or points at a staging URL) and launches a set of virtual users, each guided by a behavior profile:
| Persona | Core Traits | Typical Dashboard Interaction |
|---------|-------------|------------------------------|
| Curious | Taps every visible element, explores long‑press menus | Opens every filter, tries to long‑press chart segments to see if a detail view appears |
| Impatient | Performs actions quickly, often repeats taps before animations finish | Rapidly switches date ranges, may cause double‑load states |
| Novice | Relies on default UI, avoids advanced controls | Sticks to the preset “Last 7 days” range, never opens the date picker |
| Adversarial | Attempts to break the app with invalid input, rapid rotation, multitouch | Enters end date before start date, spams rotation, uses two‑finger pinch on chart |
| Elderly | Larger tap targets, slower gestures, prefers accessibility features | Increases font scale, enables TalkBack, uses swipe‑to‑navigate instead of taps |
| Accessibility | Relies on screen reader, high contrast mode, switch‑access | Navigates solely via TalkBack, checks for missing content‑descriptions |
| Power user | Uses shortcuts, expects keyboard‑like efficiency (if external keyboard present) | Uses hardware back button to exit, expects quick reload after rotation |
| Security‑conscious | Checks for data leakage, attempts to share sensitive info via unintended channels | Tries to drag chart image out of the app, inspects clipboard after tooltip long‑press |
Each persona maintains a memory of visited screens and dead ends, so subsequent runs become smarter—similar to how a human tester learns which areas are flaky.
### Sample SUSA CLI Invocation
After you have installed the agent (`pip install susatest-agent`), run:
susatest explore \
--apk ./app/build/outputs/apk/debug/app-debug.apk \
--personas curious impatient adversarial accessibility \
--max-depth 6 \
--output ./susa-report.json \
--telemetry-endpoint https://your-collector.example.com/ingest
- `--max-depth` limits how many navigation layers SUSA will traverse before backtracking.
- The JSON report contains: screens visited, actions taken, crashes/ANRs detected, accessibility violations, and a flow‑coverage heatmap.
### What SUSA Finds That Scripts Miss
1. **Hidden gesture conflicts** – A power‑user persona discovered that a two‑finger tap on the chart (meant to zoom) inadvertently triggered the share overflow menu because the chart’s `setOnTouchListener` consumed the event incorrectly. No scripted test included a multi‑gesture sequence.
2. **Locale‑specific crash** – The adversarial persona switched the device language to Arabic (right‑to‑left) while rapidly rotating; the chart’s axis label layout threw a `NullPointerException` due to a hard‑coded left‑to‑right assumption. The bug only appeared when the layout direction changed at runtime.
3. **Accessibility dead‑end** – The accessibility persona, relying solely on TalkBack swipe gestures, could not reach the “Export” button because it was embedded inside a `Toolbar` with `importantForAccessibility="no"`; SUSA flagged this as a violation of WCAG 2.1 1.3.1 (Info and Relationships).
4. **Data‑staleness loop** – The impatient persona tapped the refresh button every 200 ms, causing the ViewModel to launch multiple concurrent coroutines that overwrote each other’s results, ending in a stale chart that never updated. Adding a `mutateExclusive` flag fixed it.
### Integrating SUSA Findings into Your Test Suite
After a SUSA run, extract the unique failure signatures and convert them into automated regression tests:
- For each crash, add an `@Test` that reproduces the exact sequence (you can replay the action log from the JSON).
- For each accessibility violation, add an `AccessibilityChecks.Enable()` rule to the corresponding UI test or create a dedicated Compose test that asserts content‑descriptions.
- For each flow‑coverage gap (e.g., a screen never visited), add a manual exploratory charter or a new persona‑specific test case.
By treating SUSA output as a **test‑case generator**, you close the loop between exploratory discovery and deterministic verification.
## Checklist for Analytics Dashboard Testing
Copy this list into your team’s wiki or ticket template. Tick each item before marking a story as Done.
- [ ] **Happy path**: default load, date‑range change, chart type switch.
- [ ] **Error paths**: backend 5xx/4xx, empty data, corrupt cache, network timeout.
- [ ] **Edge cases**: rotation while picker open, font scale 200%, TalkBack navigation, battery‑saver, split‑screen, multi‑window.
- [ ] **Accessibility**: touch target ≥48 dp, contrast ≥4.5:1, content‑descriptions for all chart elements, live region updates for loading states.
- [ ] **Security/Privacy**: no raw event JSON in tooltips/clipboard, export uses `content://` URI, TLS enforced, no clear‑text traffic.
- [ ] **Performance**: launch ≤2 s on median device, chart redraw ≤500 ms, no memory leaks over 10 open/close cycles.
- [ ] **Locale/Time‑zone**: test at least two non‑UTC zones and two RTL languages.
- [ ] **Network conditions**: simulate 2G, metered, and intermittent loss.
- [ ] **Permission scenarios**: test with `POST_NOTIFICATIONS` revoked (API 33+).
- [ ] **Chaos validation**: run at least one randomized condition (timezone, locale, network throttle) per CI build.
- [ ] **SUSA exploration**: run a persona‑driven pass on every release candidate; log any new crashes or accessibility gaps.
- [ ] **CI gating**: unit tests, instrumented tests, MobSF scan, and SUSA report must all pass before merge.
## Final Takeaways
Testing an analytics dashboard on Android is more than verifying that a chart draws; it is validating the entire data pipeline, the UI’s resilience to real‑world device states, and the protection of user privacy.
Start with a solid test matrix that separates happy paths, error conditions, edge cases, accessibility, and security concerns. Implement unit tests for ViewModel logic, then layer Espresso or Compose UI tests that wait for idling resources and use custom matchers to inspect chart internals.
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