How to Test Reports Generation on Android (Complete Guide)

Reports are often the final deliverable that users see after completing a workflow such as expense tracking, medical data aggregation, or financial reconciliation. When a report is missing, malformed,

June 09, 2026 · 16 min read · How-To Guides

Why Testing Reports Generation Matters on Android

Reports are often the final deliverable that users see after completing a workflow such as expense tracking, medical data aggregation, or financial reconciliation. When a report is missing, malformed, or contains incorrect data, user trust erodes quickly and regulatory compliance can be jeopardized. On Android, reports frequently involve file I/O, external storage permissions, third‑party libraries (PDF, CSV, Excel), and sharing intents that traverse process boundaries. A defect in any of these layers can surface only under specific device states—low storage, locale change, or background restriction—making exhaustive testing essential.

Common Failure Modes in Reports Generation

Understanding the typical ways reports break helps focus test effort. The most frequent failure categories observed in production are:

Failure CategoryTypical SymptomRoot Cause
Data truncationReport ends mid‑row or missing fieldsBuffer size miscalculation or premature stream close
Formatting errorsMisaligned columns, wrong date locale, garbled UnicodeIncorrect use of SimpleDateFormat, hard‑coded separators
Permission denialFileNotFoundException when writing to external storageMissing WRITE_EXTERNAL_STORAGE runtime request or scoped storage mis‑handling
Storage exhaustionZero‑byte file or IOException: No space leftNot checking available space before writing large payloads
Intent resolution failureShare dialog does not appear or opens wrong appIncorrect MIME type or missing Intent.FLAG_GRANT_READ_URI_PERMISSION
Accessibility breakageTalkBack skips report preview or announces garbled textNon‑semantic views, missing content descriptions, low contrast
Security leakReport written to world‑readable directory or logged in plaintextInsecure file path, logging of sensitive data

Each of these categories can be reproduced with a targeted test matrix, which we define next.

Building a Comprehensive Test Matrix

A test matrix captures the combinations of input conditions, device states, and expected outcomes that must be verified. Below is a master table that can be copied into a test‑management tool (e.g., TestRail, Zephyr). Each row represents a distinct test case; you can add or remove columns based on your project’s needs.

TC‑IDScenarioPreconditionStepsExpected ResultNotes
RPT‑01Happy path PDF generationUser logged in, data set >0 rows, external storage granted1. Navigate to Reports screen 2. Tap “Generate PDF” 3. Wait for toast “Report saved”PDF file created in Documents/AppName/reports/ with correct name, size >0, and valid PDF header (%PDF-)Verify with PdfiumAndroid or File API
RPT‑02Happy path CSV exportSame as RPT‑011. Choose CSV format 2. Tap Export 3. Confirm save locationCSV file with correct delimiter (, or locale‑specific), UTF‑8 BOM if required, all rows presentOpen with spreadsheet app to validate
RPT‑03Error: storage fullDevice storage <10 MB free1. Fill storage with large files via ADB (adb shell dd if=/dev/zero of=/sdcard/bigfile bs=1M count=500) 2. Attempt report generationGeneration fails gracefully, shows error dialog “Insufficient storage”, no partial file leftEnsure cleanup of test files after
RPT‑04Error: permission denied (Android 13+)App targeting API 33, no MANAGE_EXTERNAL_STORAGE granted, scoped storage enabled1. Deny storage permission at runtime 2. Try to save to legacy path /sdcard/Download/Generation fails, error message “Permission required”, fallback to app‑specific directory (getExternalFilesDir)Test both denial and grant paths
RPT‑05Error: interrupted writeSimulate kill during I/O1. Start generation 2. Immediately run adb shell am kill 3. Relaunch appNo corrupted file; either file absent or contains valid partial data that app can detect and discardUse File.deleteOnExit() or check file size consistency
RPT‑06Edge case: locale change to RTLDevice language set to Arabic (right‑to‑left)1. Set locale via Settings → Language → Arabic 2. Generate reportReport layout mirrors correctly, numbers not reversed, date format follows Arabic localeVerify with screenshot comparison
RPT‑07Edge case: font scaling 200%Developer options → Font size → Largest1. Increase font size 2. Open report previewAll text readable, no clipping, layout adapts (use ConstraintLayout or ScrollView)Important for accessibility compliance
RPT‑08Edge case: dark modeSystem theme set to Dark1. Enable dark mode 2. Generate reportReport uses appropriate color contrast (WCAG AA minimum 4.5:1) for text vs backgroundCheck with accessibility scanner
RPT‑09Security: world‑readable fileApp writes to /sdcard/Download/report.pdf without MODE_PRIVATE1. Generate report 2. Run adb shell ls -l /sdcard/Download/report.pdfFile permissions are -rw------- (owner only) or app‑specific directoryPrevent data leakage
RPT‑10Privacy: log leakageReport contains PII (e.g., email)1. Enable logcat filter for package 2. Generate report 3. Observe logsNo PII appears in logcat outputUse ProGuard rules to strip logging or Timber with level checks
RPT‑11Sharing intent: correct MIMEPDF report generated1. Tap Share button 2. Choose email appIntent action ACTION_SEND, type application/pdf, URI granted with FLAG_GRANT_READ_URI_PERMISSIONVerify receiving app can open file
RPT‑12Background restrictionBattery optimization enabled for app1. Put app in background during generation 2. Wait 30 sGeneration completes or is paused/resumed correctly; no ANRUse JobScheduler or WorkManager to survive background limits
RPT‑13Multi‑window modeDevice in split‑screen with another app1. Generate report while other app occupies top half 2. Interact with both appsReport generation UI remains responsive, no layout overlap issuesTest with Android Studio emulator multi‑window
RPT‑14Interrupted network (if report fetches data)Disable Wi‑Fi/mid‑download1. Start report that pulls data from server 2. Toggle airplane mode 3. Observe behaviorGeneration shows retry or offline fallback, does not crashDepends on architecture; include if applicable
RPT‑15Concurrent report generationUser taps generate twice quickly1. Tap Generate 2. Immediately tap again before first finishesOnly one generation runs; second request either queues or shows “already generating”Prevents file corruption or duplicate files

*Tip:* When you copy this matrix into a test‑case tool, add columns for Automation Status (Manual, Automated, Planned) and Owner to track progress.

Sub‑tables for Specific Domains

For brevity in daily stand‑ups you may extract focused tables:

Accessibility Sub‑matrix

TC‑IDConditionStepsExpected
RPT‑06RTL localeSet language to Arabic, generate reportLayout mirrors, no truncated text
RPT‑07Font scale 200%Increase font size, preview reportAll text visible, no overlap
RPT‑08Dark modeEnable dark theme, generate reportContrast ratio ≥4.5:1
RPT‑??TalkBack navigationEnable TalkBack, swipe through report previewEach element announced correctly, no skipped nodes

Security/Privacy Sub‑matrix

TC‑IDConditionStepsExpected
RPT‑09World‑readable fileCheck file permissions after write-rw-------
RPT‑10Log leakageFilter logcat for package, generate reportNo PII in logs
RPT‑??Intent URI permissionShare report, verify FLAG_GRANT_READ_URI_PERMISSION setReceiving app can open file

Manual Testing Approach: Step‑by‑Step Guide

Even with automation, a disciplined manual pass catches nuances that scripts may miss, especially around user perception and device‑state interactions. Follow this procedure for each new report feature or after a major refactor.

1. Environment Setup

2. Preparing Test Data

3. Executing Happy Path Tests

  1. Navigate to the report screen.
  2. Select the desired format (PDF, CSV, Excel).
  3. Tap Generate.
  4. Wait for the success toast or notification.
  5. Open the file via a file manager or the built‑in preview.
  6. Validate:
  1. Document the outcome with a screenshot and the file’s SHA‑256 hash (adb shell sha256sum ).

4. Executing Error Path Tests

5. Logging and Evidence Collection

6. Post‑Test Cleanup

Automated Testing on Android: Tools and Frameworks

Manual validation is essential but does not scale for regression. Automated checks provide fast feedback on CI pipelines and guard against re‑introducing known defects. Below we detail the layers of automation that work best for report generation on Android.

Unit Testing Report Generation Logic

If the report creation is encapsulated in a pure Kotlin/Java class (e.g., ReportGenerator), unit tests can verify data transformation without UI or file system involvement.


// ReportGeneratorTest.kt
class ReportGeneratorTest {

    private val generator = ReportGenerator()

    @Test
    fun `pdf generator writes correct header`() {
        val data = listOf(ReportRow("Alice", 28, "alice@example.com"))
        val output = generator.generatePdf(data) // returns ByteArray
        assertTrue(output.startsWith(byteArrayOf(0x25, 0x50, 0x44, 0x46))) // "%PDF"
    }

    @Test
    fun `csv generator respects locale delimiter`() {
        val data = listOf(ReportRow("Bob", 35, "bob@example.com"))
        val csv = generator.generateCsv(data, Locale.GERMANY)
        assertTrue(csv.contains(";")) // German locale uses semicolon
    }
}

Run these tests with ./gradlew testAndroidUnit. They execute on the JVM, providing sub‑second feedback.

Instrumented UI Tests with Espresso

Espresso validates that the UI triggers the generation flow and that the success state is observable.


// ReportGenerationTest.kt
@RunWith(AndroidJUnit4::class)
class ReportGenerationTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class)

    @Test
    fun generatePdf_showsSuccessToast_andCreatesFile() {
        // Load sample data via debug menu
        onView(withId(R.id.btnLoadSample)).perform(click())
        onView(withId(R.id.btnGeneratePdf)).perform(click())

        // Wait for toast
        onView(withText(containsString("Report saved")))
            .inRoot(IsPlatformToast())
            .check(matches(isDisplayed()))

        // Verify file exists using a helper that runs on a background thread
        val context = ApplicationProvider.getApplicationContext()
        val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),
                       "reports/report_${System.currentTimeMillis()}.pdf")
        assertTrue("File should exist", file.exists())
        // Optional: check PDF header
        val header = FileInputStream(file).readNBytes(4)
        assertArrayEquals(byteArrayOf(0x25, 0x50, 0x44, 0x46), header)
    }
}

Key points:

UIAutomator for File System Checks

When you need to inspect the file system outside the app’s sandbox (e.g., verify that a report appears in the public Downloads folder), UIAutomator works well because it can run with broader permissions.


// ReportFileCheck.java
public class ReportFileCheck extends UiAutomatorTestCase {

    public void testPdfAppearsInDownloads() throws Exception {
        // Launch the app and trigger generation
        final String pkg = getTargetContext().getPackageName();
        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        device.launchActivity(new Intent(Intent.ACTION_MAIN)
                .setPackage(pkg)
                .setCategory(Intent.CATEGORY_LAUNCHER));

        // Click generate button (replace with actual resource ID)
        UiObject2 genBtn = device.findObject(By.res(pkg, "id/generatePdf"));
        genBtn.click();

        // Wait for toast
        UiObject2 toast = device.findObject(By.text(contains("Report saved")));
        assertNotNull(toast);

        // Check file in Downloads
        File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        File[] files = downloads.listFiles((dir, name) -> name.endsWith(".pdf"));
        assertTrue("No PDF found in Downloads", files.length > 0);
        // Validate first file’s header
        FileInputStream fis = new FileInputStream(files[0]);
        byte[] header = new byte[4];
        fis.read(header);
        assertArrayEquals(new byte[]{(byte)0x25, 0x50, 0x44, 0x46}, header);
        fis.close();
    }
}

Run with ./gradlew connectedAndroidTest. This test can be tagged (androidTestAnnotation) to run only on nightly builds if it is slower.

Leveraging SUSA Autonomous Exploration (Mention)

SUSA’s autonomous agent can be pointed at the APK or a internal test build. It will explore the reports flow using its built‑in personas (curious, impatient, novice, etc.) without any test scripts. During exploration it:

To invoke SUSA locally:


pip install susatest-agent
susatest run --apk path/to/app-debug.apk --goal "reports" --personas all --output ./susa-reports

The agent will produce a JSON summary with discovered issues, each tagged with the responsible persona (e.g., “impatient” found a double‑tap crash).

Integrating with CI/CD

Code Snippets: Espresso Test for PDF Report Generation

Below is a self‑contained example that demonstrates how to verify PDF content using the PdfiumAndroid library, which can parse PDF pages and extract text for assertions.


// PdfReportVerificationTest.kt
@RunWith(AndroidJUnit4::class)
class PdfReportVerificationTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class)

    @Test
    fun generatedPdfContainsExpectedData() {
        // Preload known data
        onView(withId(R.id.btnLoadFixture)).perform(click())
        onView(withId(R.id.btnGeneratePdf)).perform(click())

        // Wait for generation completion
        onView(withText("Report saved"))
            .inRoot(IsPlatformToast())
            .check(matches(isDisplayed()))

        // Locate the newest PDF in app‑specific docs
        val context = ApplicationProvider.getApplicationContext()
        val docsDir = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "reports")
        val pdfFile = docsDir.listFiles()
                .filter { it.name.endsWith(".pdf") }
                .maxByOrNull { it.lastModified() }
                ?: error("No PDF found")

        // Load PDF with PdfiumAndroid
        val document = PdfDocument.openFile(pdfFile.absolutePath)
        val page = document.getPage(0)
        val text = page.text
        page.close()
        document.close()

        // Assert that known strings appear
        assertTrue(text.contains("Alice Smith"))
        assertTrue(text.contains("2024-09-26"))
        // Optional: check that no extra pages were added
        assertEquals(1, document.pagesCount)
    }
}

Explanation

  1. The test loads a deterministic fixture so the expected output is known.
  2. After generation, it waits for the success toast (a reliable UI signal).
  3. It scans the app‑specific reports directory for the most‑recent PDF, avoiding race conditions with parallel test runs.
  4. PdfiumAndroid extracts the text from the first page; assertions confirm that the data from the fixture is present.
  5. Resources are closed promptly to avoid file‑handle leaks.

Code Snippets: UIAutomator to Validate Report Sharing Intent

Sharing a report often involves an Intent with a content:// URI. This UIAutomator test confirms that the intent is correctly formed and that the receiving app can read the file.


// ShareIntentVerification.java
public class ShareIntentVerification extends UiAutomatorTestCase {

    public void testShareIntentHasCorrectMimeAndUriPermission() throws Exception {
        String pkg = getTargetContext().getPackageName();
        UiDevice device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());

        // Launch app and navigate to report screen
        device.launchActivity(new Intent(Intent.ACTION_MAIN)
                .setPackage(pkg)
                .setCategory(Intent.CATEGORY_LAUNCHER));
        device.wait(Until.hasObject(By.pkg(pkg).depth(0)), 5000);

        // Load sample data (assuming a debug button)
        device.findObject(By.res(pkg, "id/loadSample")).click();
        device.wait(Until.hasObject(By.text("Data loaded")), 3000);

        // Trigger share
        device.findObject(By.res(pkg, "id/sharePdf")).click();

        // Wait for the chooser to appear
        UiObject2 chooser = device.wait(Until.hasObject(By.text("Share via")), 5000);
        assertNotNull(chooser);

        // Retrieve the intent sent to the chooser via instrumentation
        Intent sentIntent = getInstrumentation().getTargetContext()
                .getSystemService(Context.class)
                .getSystemService(NotificationManager.class)
                .getActiveNotifications()[0]
                .notification
                .contentIntent
                .getIntent();

        assertNotNull(sentIntent);
        assertEquals(Intent.ACTION_SEND, sentIntent.getAction());
        assertEquals("application/pdf", sentIntent.getType());

        Uri uri = (Uri) sentIntent.getParcelableExtra(Intent.EXTRA_STREAM);
        assertNotNull(uri);
        // Verify that the URI grants read permission to the chooser
        int flag = sentIntent.getFlags();
        assertTrue((flag & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0);

        // Optionally, launch a known viewer (e.g., Chrome) and confirm it opens
        ComponentName chrome = new ComponentName("com.android.chrome", "com.google.android.apps.chrome.Main");
        device.executeShellCommand("am start -n " + chrome.flattenToString() + " -d " + uri.toString());
        device.wait(Until.hasObject(By.clazz("android.webkit.WebView")), 8000);
    }
}

What this test does

Edge Cases that Only Appear in Production

Even with exhaustive matrices, certain conditions surface only after the app reaches a broad user base. Below are the most insidious ones, why they escape lab testing, and how to mitigate them.

Edge CaseWhy It’s Missed in LabDetection StrategyMitigation
Low‑storage kill during writeEmulators usually start with ample free space; CI agents often clean storage before each run.Use adb shell shell pm set-install-location 2 to force install on SD, then fill storage with a script that leaves <5 MB free. Run generation in a loop.Check StorageManager.getStorageStats() before writing; show warning and abort if free space < 2× expected file size.
Interrupted write due to process killUnit tests mock I/O; Espresso tests wait for UI completion, not actual kill.Inject a CountDownLatch in the repository layer that, when triggered, calls android.os.Process.killProcess(Process.myPid()). Verify that no half‑written file remains.Write to a temporary file in the app‑specific cache directory, then rename atomically (File.renameTo) after successful close.
Locale‑specific number formattingTest devices often default to en_US; testers may not switch to ar_EG or th_TH.Automate locale matrix via adb shell setprop persist.sys.language &&adb shell setprop persist.sys.country &&adb shell stop &&adb shell start.Use NumberFormat.getInstance(locale) and DateFormat.getDateInstance(DateFormat.DEFAULT, locale) everywhere; avoid hard‑coded patterns.
Font scaling breaking layoutDesigners review on default font size; QA may not enable the largest size.Run UI tests with adb shell settings put system font_scale 2.0 and assert that no view’s height exceeds parent constraints.Use wrap_content with maxLines and ellipsize, or rely on ConstraintLayout chains that adjust.
Dark mode contrast failuresDark mode toggles are often overlooked; contrast checks require specialized tools.Integrate Android’s AccessibilityTestFragment (from the Accessibility Test Framework) into your instrumented suite to run on each build.Define color resources with ?attr/colorOnBackground and verify contrast ≥4.5:1 using the framework’s API.
Multi‑window resize causing UI jitterMost tests run in full‑screen mode; split‑screen is rarely exercised.Use UIAutomator to drag the divider (adb shell cmd window resize --mainRatio 0.3) while the report generation is in progress.Ensure that all UI elements use ConstraintLayout or LinearLayout with weight; avoid fixed dp dimensions that exceed smaller pane width.
Background restriction stopping WorkManagerDoze mode and App Standby can defer work; tests often disable battery optimizations locally.Enable battery optimization for the app via adb shell cmd appops set RUN_IN_BACKGROUND ignore, then trigger a work request and verify it completes after the maintenance window.Use setExpedited(true) for urgent work, or provide a foreground service with a persistent notification when immediate completion is required.
Scoped storage permission regression on Android 13+Developers test on Android 12 where legacy storage still works; they forget to handle the new model.On an API 33 device, deny MANAGE_EXTERNAL_STORAGE and attempt to write to Environment.DIRECTORY_DOWNLOADS. Verify fallback to getExternalFilesDir.Always use MediaStore APIs for public‑facing files, or save to app‑specific directories and use ACTION_OPEN_DOCUMENT for user‑chosen locations.
Concurrent report generation causing file clashesManual testing rarely taps the generate button twice in quick succession.Use a monkey runner script: adb shell monkey -p -c android.intent.category.LAUNCHER --throttle 200 --count 100 and monitor for duplicate file names or crashes.Make the generation function synchronized or use a Mutex from Kotlin coroutines; generate file names with UUID or timestamp + random suffix.
Network loss mid‑report (if data is fetched)Unit tests mock the network; UI tests often have constant connectivity.Toggle airplane mode (adb shell cmd connectivity airplane-mode enable) after the request starts but before it completes.Implement retry with exponential backoff, cache intermediate results, and show an offline placeholder.

Production‑level monitoring

Checklist for Reports Generation Testing

Copy this list into your team’s wiki or Definition of Done (DoD) for any report‑related feature.

Closing

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