How to Test Avatar Upload on Android (Complete Guide)

Avatar upload is a deceptively simple feature that touches many parts of an Android app: UI components that accept image input, permission handling, background services that resize or compress images,

March 31, 2026 · 16 min read · How-To Guides

Why Avatar Upload Deserves Focused Testing

Avatar upload is a deceptively simple feature that touches many parts of an Android app: UI components that accept image input, permission handling, background services that resize or compress images, network calls that send multipart payloads, storage logic that persists the result, and UI updates that reflect the new picture. Because the flow involves user‑generated media, it is a frequent source of crashes, ANRs, silent failures, and privacy leaks. In production, a broken avatar upload can prevent users from completing onboarding, trigger GDPR‑relevant data exposure, or cause the app to appear unpolished when the placeholder image never changes. Treating it as a “nice‑to‑have” widget leads to under‑tested code paths that surface only after a release, when users encounter odd behavior such as the picker closing without feedback, the upload stalling at 0 %, or the server rejecting a file because of an unexpected MIME type. A dedicated test effort catches these issues early, reduces support tickets, and protects the brand image that the avatar itself represents.

Common Failure Modes Seen in the Wild

Before diving into test design, it helps to catalog the ways avatar upload breaks after it leaves the QA environment. The list below is drawn from real‑world crash reports, ANR traces, and user complaints collected from several Android apps that expose a profile picture picker.

Failure CategoryTypical SymptomRoot Cause (examples)
Permission mishandlingPicker never opens; toast says “Permission denied”Missing runtime request for READ_EXTERNAL_STORAGE or CAMERA on Android 13+; using legacy manifest‑only permission
Picker integration bugsChooser shows duplicate entries or crashes when selecting from Google PhotosImproper Intent flags (missing Intent.FLAG_GRANT_READ_URI_PERMISSION)
Image processing OOMApp crashes with OutOfMemoryError after selecting a 12 MP photoDecoding bitmap at original size before down‑sampling; not using BitmapFactory.Options.inSampleSize
Upload stallsProgress bar stays at 0 % or 100 % indefinitelyNetwork timeout not handled; missing retry logic; server returns 413 but client treats as success
Incorrect MIME/typeServer rejects file with “Unsupported media type”Using ContentResolver.getType() on a Uri that returns null; falling back to image/jpeg for HEIC files
Silent failureUI shows old avatar; no error toastSwallowing exceptions in AsyncTask/Worker; not observing LiveData updates
Accessibility gapsTalkBack does not announce success/failureMissing contentDescription on buttons; not using announceForAccessibility
Security/privacy leaksSelected image appears in app’s external cache readable by other appsStoring raw image to getExternalCacheDir() without setPrivate(); logging image paths
Rotation/orientation lossUploaded avatar appears sidewaysNot honoring Exif orientation matrix before encoding to JPEG
Duplicate uploadsSame image sent multiple times on rapid tapsNo debouncing; multiple click listeners firing

These patterns show that avatar upload is not just a UI test; it intersects with permissions, media handling, networking, concurrency, and accessibility. A test matrix that addresses each dimension is essential.

Comprehensive Test Matrix for Avatar Upload

The following table expands the failure categories into concrete test cases. Each row includes a short description, the expected outcome, and the severity level (P1 = blocker, P2 = high, P3 = medium). Use this matrix as a checklist when designing both manual and automated tests.

IDCategoryTest Case DescriptionSteps (high‑level)Expected ResultSeverity
A1Happy path – GallerySelect a JPEG from device gallery, crop if needed, confirm uploadOpen profile → tap avatar → choose “Gallery” → navigate to image → confirm → wait for upload success toastAvatar updates instantly; no error messages; network log shows 200 OK with correct multipart payloadP1
A2Happy path – CameraCapture a photo with camera, use it as avatarSame as A1 but choose “Camera” → take picture → accept → uploadAvatar updates; image is correctly oriented; no permission dialog after grantP1
B1Permission denied – StorageDeny READ_EXTERNAL_STORAGE when picker launchesDeny permission via app settings → attempt gallery pickerPicker shows system dialog to grant permission; after denial, app shows rationale toast and does not crashP2
B2Permission denied – CameraDeny CAMERA permission before taking pictureDeny CAMERA → attempt camera pickerSame as B1 but for camera; app handles gracefullyP2
C1Large image OOMSelect a 20 MP photo (≈10 MB) from galleryPick largest available image → confirmApp downsamples before decoding; no OOM; upload succeeds (may be slower)P1
C2Zero‑byte fileAttempt to upload a file with size 0 B (create empty file via adb)adb shell touch /sdcard/empty.jpg → select via picker → confirmApp shows error toast “Invalid image”; no crashP2
C3Unsupported format (HEIC)Choose a HEIC image (common on newer devices)Transfer HEIC file to device → select → confirmApp either converts to JPEG/PNG or shows clear unsupported‑type messageP2
D1Network loss mid‑uploadEnable airplane mode after selecting image but before upload completesStart upload → toggle airplane mode → waitApp shows retry mechanism or error toast; does not hang; partial upload cleaned upP1
D2Server returns 413 Payload Too LargeMock server to reject oversized imageUse a tool like mitmproxy to return 413 → select large image → confirmApp displays “File too large” toast; does not crash; allows user to pick smaller imageP2
D3Invalid MIME from ContentResolverProvide a Uri with null MIME type (e.g., file://)Use content:// scheme pointing to a non‑media file → select → confirmApp falls back to extension‑based detection or shows error; no NPEP2
E1Accessibility – TalkBack announcementVerify TalkBack reads success/failureEnable TalkBack → perform A1 or C3 → listenTalkBack announces “Avatar updated” or “Upload failed, please try again”P2
E2Touch target sizeEnsure avatar button meets 48 dp minimumUse UI Automator to get bounds → verify ≥48 dpPasses Android accessibility guidelineP2
E3Color contrastCheck avatar placeholder/icon contrast against backgroundUse accessibility scanner → verify contrast ratio ≥4.5:1Passes WCAG AAP2
F1Private storage leakageVerify selected image not written to world‑readable locationAfter upload, inspect /sdcard/Android/data//cache and external storageNo raw image file outside app‑private directoriesP1
F2Image metadata strippingConfirm EXIF GPS data removed before uploadPick geotagged photo → intercept network call → check payloadNo GPS tags in uploaded multipart bodyP2
G1Rapid double tapTap avatar button twice quicklyTwo taps within 200 ms → observeOnly one picker launches; second tap ignored or shows “Already picking” toastP2
G2Orientation change mid‑flowRotate device while picker is openStart picker → rotate to landscape → confirm selectionPicker dismisses gracefully or retains state; no crashP2
H1Cross‑profile user flowTest avatar upload for a newly created account vs. existing accountCreate new account → go to profile → upload avatar → log out → log in → verify avatar persistsAvatar persists across sessions; no mismatchP1
H2Data cleanup on logoutEnsure avatar image removed from private cache on logoutUpload avatar → log out → check cache directoryAvatar file deleted or overwritten with default placeholderP2

How to Use the Matrix

Manual Testing Approach – Step‑by‑Step

Even with automation, a manual exploratory pass catches nuances that scripts may miss, especially around device‑specific UI quirks and permission dialogs. Below is a reproducible manual procedure that you can follow on any Android device or emulator.

  1. Environment preparation
  1. Baseline happy path
  1. Permission denial scenarios
  1. Camera path
  1. Large file & OOM test
  1. Error injection
  1. Accessibility check
  1. Privacy & storage verification
  1. Stress & concurrency
  1. Logout persistence

Throughout the manual pass, keep a notebook or spreadsheet to record each test ID, the device model, Android version, and any deviations. This log becomes valuable when reproducing issues later.

Automated Testing – Strategies and Tooling

Manual testing is indispensable for exploratory work, but regression safety relies on automated checks that run on every commit. Android offers several layers for testing avatar upload, from unit‑level validation of image‑processing logic to end‑to‑end UI tests that exercise the full flow. Below we detail practical approaches, complete with code snippets you can drop into a typical Android Studio project.

1. Unit Tests for Image Preparation

Isolate the bitmap transformation and MIME‑type detection logic. Use Robolectric or plain JVM tests (if you avoid Android SDK calls) to verify:


// AvatarImageProcessorTest.kt
@RunWith(RobolectricTestRunner::class)
class AvatarImageProcessorTest {

    private val processor = AvatarImageProcessor()

    @Test
    fun `downsamples large bitmap`() {
        val largeBitmap = BitmapFactory.decodeResource(
            ApplicationProvider.getApplicationContext(),
            R.drawable.test_20mp
        )
        val result = processor.prepareForUpload(largeBitmap, maxDimension = 1024)
        assertTrue(result.width <= 1024 && result.height <= 1024)
    }

    @Test
    fun `applies exif rotation`() {
        val uri = Uri.parse("file:///sdcard/test_rotated.jpg")
        val input = ApplicationProvider.getApplicationContext()
            .contentResolver
            .openInputStream(uri)!!
        val original = BitmapFactory.decodeStream(input)
        val processed = processor.applyExifOrientation(original, uri)
        // Assuming the test image has a 90° clockwise rotation tag
        assertEquals(original.height, processed.width)
        assertEquals(original.width, processed.height)
    }
}

These tests run fast on the JVM and give confidence that the core image pipeline won’t cause OOM or corruption.

2. Integration Tests with Espresso

Espresso excels at verifying UI interactions within the same process. Use it to test the picker launch, permission handling, and basic success/failure UI feedback. Because Espresso cannot directly interact with the system picker, we replace it with a mock chooser using Android’s Intent interception via IntentsTestRule.


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

    @get:Rule
    val intentsRule = IntentsTestRule(ProfileActivity::class.java, true, false)

    @Test
    fun gallerySelection_showsSuccessToast() {
        // Grant runtime permission before launching activity
        grantPermission(Manifest.permission.READ_EXTERNAL_STORAGE)

        // Launch activity
        intentsRule.launchActivity(Intent())

        // Click avatar
        onView(withId(R.id.avatar_image)).perform(click())

        // Verify system picker intent is fired
        intending(toPackage("com.android.documentsui"))
            .respondWith(
                Instrumentation.ActivityResult(Activity.RESULT_OK,
                    Intent().setData(Uri.parse("content://media/external/images/123")))
            )

        // Confirm crop (if any) – here we assume no crop UI
        onView(withId(R.id.button_crop_ok)).perform(click())

        // Verify toast
        onView(withText(R.string.avatar_updated))
            .inRoot(IsPlatformToast())
            .check(matches(isDisplayed()))
    }

    @Test
    fun permissionDenied_showsRationale() {
        revokePermission(Manifest.permission.READ_EXTERNAL_STORAGE)
        intentsRule.launchActivity(Intent())
        onView(withId(R.id.avatar_image)).perform(click())
        onView(withText(R.string.permission_storage_rationale))
            .inRoot(withDecorView(not(is(intentsRule.activity.window.decorView))))
            .check(matches(isDisplayed()))
    }
}

Notes

3. End‑to‑End UI Tests with UI Automator

UI Automator can interact with system dialogs (permission picker, recent apps) and works across app boundaries, making it suitable for testing the actual system picker and permission flows. Below is a script that validates the happy path using UI Automator 2.x.


// AvatarUploadUiAutomatorTest.java
@RunWith(AndroidJUnit4.class)
public class AvatarUploadUiAutomatorTest {

    private static final String PACKAGE = "com.example.myapp";
    private UiDevice device;

    @Before
    public void setUp() {
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        device.pressHome();
    }

    @Test
    public void testGalleryUpload_success() throws UiObjectNotFoundException {
        // Clear app data to start fresh
        device.executeShellCommand("pm clear " + PACKAGE);

        // Launch app
        Intent intent = new Intent();
        intent.setPackage(PACKAGE);
        intent.setAction(Intent.ACTION_MAIN);
        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        Context context = InstrumentationRegistry.getTargetContext();
        context.startActivity(intent);

        // Wait for profile screen
        device.wait(Until.hasObject(By.desc("Profile")), 5000);

        // Click avatar
        UiObject avatar = device.findObject(new UiSelector()
                .resourceId(PACKAGE + ":id/avatar_image"));
        avatar.clickAndWaitForNewWindow();

        // Choose Gallery from chooser
        UiObject galleryOption = device.findObject(new UiSelector()
                .textContains("Gallery"));
        galleryOption.clickAndWaitForNewWindow();

        // In system picker, select first image
        UiObject firstImage = device.findObject(new UiSelector()
                .className("android.widget.ImageView")
                .instance(0));
        firstImage.clickAndWaitForNewWindow();

        // Confirm crop (if present)
        UiObject cropOk = device.findObject(new UiSelector()
                .text("OK"));
        if (cropOk.exists()) {
            cropOk.click();
        }

        // Verify success toast
        UiObject toast = device.findObject(new UiSelector()
                .className("android.widget.Toast"));
        assertTrue(toast.waitForExists(5000));

        // Verify avatar updated (simple check: placeholder gone)
        UiObject newAvatar = device.findObject(new UiSelector()
                .resourceId(PACKAGE + ":id/avatar_image")
                .checked(false));
        assertTrue(newAvatar.waitForExists(5000));
    }
}

Why UI Automator?

4. Network Mocking with MockWebServer

To verify that the app correctly builds the multipart request and handles various server responses, use OkHttp’s MockWebServer in an instrumented test. This lets you assert on request headers, body parts, and response codes without hitting a real backend.


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

    private lateinit var mockWebServer: MockWebServer
    private lateinit var avatarRepo: AvatarRepository

    @Before
    fun setUp() {
        mockWebServer = MockWebServer()
        mockWebServer.start()
        val okHttpClient = OkHttpClient.Builder()
            .url(mockWebServer.url("/api/avatar"))
            .build()
        avatarRepo = AvatarRepository(okHttpClient)
    }

    @After
    fun tearDown() = mockWebServer.shutdown()

    @Test
    fun `uploads image and returns 200`() {
        // Enqueue a successful response
        mockWebServer.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody("{\"url\":\"https://cdn.example.com/avatars/abc123\"}"))

        // Create a tiny JPEG in memory
        val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.RGB_565)
        val outputStream = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
        val imageBytes = outputStream.toByteArray()

        // Call repository
        val result = avatarRepo.uploadAvatar(imageBytes)

        assertTrue(result.isSuccess)
        val recordedRequest = mockWebServer.takeRequest()
        assertEquals("multipart/form-data", recordedRequest.getHeader("Content-Type"))
        // Verify part name and filename
        assertTrue(recordedRequest.getBody().readUtf8()
            .contains("Content-Disposition: form-data; name=\"avatar\"; filename=\"image.jpg\""))
    }

    @Test
    fun `handles 413 payload too large`() {
        mockWebServer.enqueue(MockResponse()
            .setResponseCode(413)
            .setBody("File too large"))

        val bitmap = Bitmap.createBitmap(2000, 2000, Bitmap.Config.RGB_565)
        val output = ByteArrayOutputStream()
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, output)
        val result = avatarRepo.uploadAvatar(output.toByteArray())

        assertFalse(result.isSuccess)
        assertEquals(Result.Error::class.java, result::class.java)
    }
}

These tests guarantee that the networking layer behaves correctly under success, client error, and server error conditions.

5. Using Firebase Test Lab for Device Matrix

Running Espresso/UI Automator tests on a single emulator does not capture device‑specific quirks (different OEM camera apps, varied storage providers, manufacturer‑specific permission dialogs). Firebase Test Lab lets you execute your instrumented test suite across a matrix of physical devices and Android versions. A typical gcloud command looks like:


gcloud firebase test android run \
    --type instrumentation \
    --app app-debug.apk \
    --test app-debug-test.apk \
    --device model=Pixel3,version=33,locale=en,orientation=portrait \
    --device model=SamsungGalaxyS21,version=31,locale=en,orientation=landscape \
    --timeout 90s

You can shard the test suite to run multiple tests in parallel, drastically reducing feedback time. Incorporate this step into your CI pipeline (GitHub Actions, Bitrise, etc.) so that every PR is validated on a representative set of devices.

Edge Cases That Only Appear in Production

Even the most thorough test matrix can miss issues that surface only under real‑world usage patterns. Below are several production‑only observations that have caused severe bugs in avatar uploads, along with suggestions on Android developers should watch for.

  1. Intermittent storage provider changes – Some file managers (e.g., Solid Explorer, CX File Explorer) return a content:// Uri that points to a DocumentFile backed by a cloud service (Google Drive, Dropbox). The ContentResolver.openInputStream may return a filtered stream that does not support mark/reset, causing the down‑sampling code to fail silently. Mitigation: always copy the stream to a temporary file in cache before decoding.
  1. Background throttling on Android 12+ – If the upload is started from a foreground service but the app quickly moves to the background, the system may restrict network access, leading to apparent “stalls”. Using WorkManager with setExpedited(true) (for Android 12) or a foreground service with a persistent notification avoids this.
  1. Battery‑optimization whitelist – On certain Xiaomi or Oppo devices, aggressive battery saver kills background threads, causing the upload to be aborted after a few seconds. Adding a android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS check and guiding users to whitelist the app can reduce reports.
  1. Locale‑dependent MIME detection – Some OEM gallery apps return MIME types with uppercase extensions (.JPG) while the server expects lowercase. Normalizing MIME strings (mimeType.toLowerCase(Locale.US)) prevents 415 Unsupported Media Type errors.
  1. Network quality fluctuations – Users on 2G or fluctuating Wi‑Fi may experience partial uploads where the server receives a truncated multipart body. Implementing retry with exponential backoff and validating the Content-Length header before accepting the upload mitigates silent corruption.
  1. Multipart boundary collisions – When constructing the multipart body manually (instead of using a library like OkHttp), a poorly chosen boundary string can appear inside the image data, causing the server to misparse the request. Always rely on a well‑tested HTTP client that generates random boundaries.
  1. Concurrent uploads from multiple accounts – If the app supports account switching and does not clear the previous avatar’s Uri, a rapid account change can cause the wrong image to be uploaded. Ensure that each upload operation uses a freshly scoped Uri and clears any stale references after completion.
  1. Accessibility service interference – Certain accessibility services (e.g., screen readers, switch control) inject synthetic clicks that can trigger the avatar button twice in rapid succession, bypassing debounce logic. Test with TalkBack and Switch Access enabled to confirm that your debounce or mutex guards against duplicate picker launches.

By incorporating these observations into your test matrix (e.g., adding a test case for “content Uri from cloud storage” or “battery optimization kill”), you reduce the gap between lab and production.

Quick‑Reference Checklist

Copy this list into your test plan or wiki. Tick each item as you verify it for a given release.

If any item fails, mark the associated test ID from the matrix and prioritize a fix before release.

Closing Takeaways

Avatar upload may look like a trivial UI widget, but it is a convergence point for permissions, media handling, networking, concurrency, accessibility, and privacy. A disciplined testing strategy combines:

  1. A detailed matrix that enumerates happy paths, error paths, edge cases, accessibility, and security checks.
  2. Manual exploratory steps that catch device‑specific quirks, permission dialogs, and real‑world usability issues.
  3. Automated layers—unit tests for image processing, Espresso for UI logic, UI Automator for system dialogs, MockWebServer for network contracts, and Firebase Test Lab for broad device compatibility.
  4. Production‑aware observations such as cloud storage URIs, battery‑optimization kills, and locale‑specific MIME strings that only emerge under actual user load.

By treating avatar upload as a first‑class feature with its own test plan, you reduce the likelihood of embarrassing crashes, GDPR‑relevant data leaks, and poor user experiences that tarnish the very identity the avatar is meant to represent.

When you have the budget and desire to go beyond scripted checks, consider an autonomous, persona‑driven explorer like the one offered by SUSATest. It can roam the app with varied user profiles (curious, impatient, elderly, etc.) and surface scenarios—like a power user rapidly switching accounts while the picker is open—that static test scripts rarely anticipate. Combining such exploratory power with the structured matrix and automation described above gives you confidence that avatar upload works not just in the lab, but in the hands of every real user who taps that little circle to show their face.

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