Lesson 3 of 4
Lesson 03 — Test Design for Mobile
Title: Gestures, permissions, and real-world test scenarios
Description: Learn how to design comprehensive test cases for mobile apps, covering gestures, permissions, interruptions, network conditions, app lifecycle, and orientation — all the scenarios that separate a thorough mobile QA engineer from a beginner.
Why it matters for QA: A test suite that only checks the happy path will pass in CI and fail for real users. Mobile users live in the real world: they get phone calls mid-checkout, they rotate their phone sideways, they lose network connectivity in the subway. Your job as a QA engineer is to catch those failures before your users do.
1. Mobile-specific test types you must cover
Most QA beginners start by testing the happy path — the ideal scenario where everything works as expected. This is a good starting point, but mobile apps need much more coverage.
Here are the essential test categories for mobile:
Happy path (the baseline)
The happy path is the most common user scenario: the user does what they are supposed to do, everything works correctly.
Example for a login screen:
- User opens the app
- User enters a valid email and password
- User taps "Login"
- User sees the home screen
You must always write the happy path first. Everything else builds on it.
Permissions: grant, deny, and revoke
Mobile apps ask users for permissions: access to the camera, microphone, location, contacts, and more. Each permission has three important test scenarios:
Grant: The user allows the permission. The feature works as expected.
Deny: The user refuses the permission. The app must handle this gracefully — show a helpful message, offer an alternative, or disable the feature without crashing.
Revoke: The user initially granted the permission, but later goes into Settings and turns it off. The next time they use the feature, the app must detect the missing permission and respond appropriately.
In practice: Many apps crash or show a confusing blank screen when a permission is denied. Always test the deny path explicitly.
Interruptions: incoming calls, notifications, low battery
While your user is in the middle of filling out a form or completing a purchase, real life happens. Test these scenarios:
- Incoming phone call: User receives a call while using the app. After the call, they return to the app. Is their progress saved? Is the app in the same state?
- Push notification: A notification appears while the user is on a specific screen. They tap it and are taken somewhere else. They press Back. Where do they end up?
- Low battery warning: The system shows a "Battery low (15%)" dialog. Does the app handle this correctly?
- System update notification: The OS shows an update notification. Does it disrupt the user's flow?
Network conditions: offline, slow 3G, WiFi to cellular switch
Real users are not always on fast WiFi. Test:
- No network: What does the app show when there is no internet? A friendly "No connection" message or a crash/empty screen?
- Slow network (3G, 2G): Does the app time out gracefully? Are loading states shown?
- Network loss mid-request: The user submits a form and the network drops exactly as the request is sent. Does the app show an error? Does it retry?
- WiFi to cellular switch: The user moves out of WiFi range during a video stream or file upload. Does the app continue? Does it fail silently?
How to simulate network conditions:
- Android emulator: Network throttling in emulator settings
- iOS simulator: Network Link Conditioner (developer tool)
- Real device: Enable Airplane Mode, or use device developer settings
App lifecycle: background, foreground, kill, and relaunch
Mobile operating systems manage app memory. An app can be:
- Foreground: The user is actively using it
- Background: The user pressed Home and the app is running behind the scenes
- Killed: The OS terminated the app to free memory, or the user force-closed it
Test these transitions:
- Background → Foreground: User presses Home, then returns to the app 1 minute later. Is the state preserved?
- Background → Foreground (long time): User leaves the app for 30 minutes. Many apps refresh data or require re-login. Is this handled well?
- Kill → Relaunch: User force-closes the app from the task switcher. On relaunch, is the app in a clean state?
- OS kills background app: While the app is in the background, the OS kills it (common on low-RAM devices). When the user returns, what happens?
Orientation: portrait and landscape
Many apps support both portrait (vertical) and landscape (horizontal) orientation. Test:
- Does the layout adjust correctly when rotated?
- Does the content preserve state? (If the user typed text into a form, is it still there after rotation?)
- On Android specifically: rotating the screen causes an Activity recreation, which is a very common source of bugs
In practice: Many developers only design for portrait mode. If your app supports landscape, rotation tests are essential.
2. Writing mobile test cases — a step-by-step approach
A good test case has four components:
- Precondition: The state of the app before the test begins
- Steps: Exactly what the user does, one action at a time
- Expected result: What should happen
- Actual result: What actually happened (filled in during test execution)
Process: from happy path to comprehensive coverage
Step 1: Write the happy path first Start with what a normal user does when everything works. This is your baseline.
Step 2: Add negative paths What if the user provides invalid data? What if a required service is down? What if the user skips a required step?
Step 3: Add edge cases What about empty states (no data to display), error states (server returned an error), first-launch states (app opened for the first time), and boundary values (password that is exactly 8 characters, the minimum)?
Step 4: Add mobile-specific scenarios Now add the scenarios from Section 1: permissions, interruptions, network conditions, orientation, and lifecycle.
Example: login screen test cases
| # | Test case | Precondition | Steps | Expected result |
|---|---|---|---|---|
| 1 | Happy path login | App installed, user has a valid account, no active session | 1. Open app. 2. Enter valid email. 3. Enter valid password. 4. Tap "Login" | User is navigated to the home screen. Welcome message shown |
| 2 | Wrong password | Same as above | 1. Open app. 2. Enter valid email. 3. Enter wrong password. 4. Tap "Login" | Error message shown: "Incorrect password". User stays on login screen |
| 3 | Empty email field | App on login screen | 1. Leave email empty. 2. Enter any password. 3. Tap "Login" | Validation error appears under email field. Login not attempted |
| 4 | Empty password field | App on login screen | 1. Enter valid email. 2. Leave password empty. 3. Tap "Login" | Validation error appears under password field. Login not attempted |
| 5 | Invalid email format | App on login screen | 1. Enter "notanemail" in email field. 2. Enter any password. 3. Tap "Login" | Validation error: "Please enter a valid email address" |
| 6 | Biometric login | User has fingerprint enrolled, biometric login enabled in settings | 1. Open app. 2. Tap "Use fingerprint". 3. Authenticate with fingerprint | User is logged in and navigated to home screen |
| 7 | Biometric denied | Same as above | 1. Open app. 2. Tap "Use fingerprint". 3. Cancel biometric prompt | User remains on login screen. "Login cancelled" message shown |
| 8 | Login with no network | No internet connection | 1. Disable WiFi and cellular. 2. Open app. 3. Enter valid credentials. 4. Tap "Login" | Error message: "No internet connection. Please check your network." |
| 9 | App backgrounded mid-login | User on login screen with email entered | 1. Type email. 2. Press Home button. 3. Wait 1 minute. 4. Return to app | Email field still contains the typed text. App is on login screen |
| 10 | Screen rotation on login | App on login screen with email entered | 1. Type email. 2. Rotate device to landscape. 3. Rotate back to portrait | Email text is preserved. UI layout adjusts correctly in both orientations |
| 11 | Login interrupted by call | User mid-login process | 1. Enter credentials. 2. Simulate incoming phone call (accept or decline). 3. Return to app | App resumes on login screen with data intact |
| 12 | Account locked (too many attempts) | Account exists | 1. Enter valid email. 2. Enter wrong password 5 times in a row | Account locked message shown. Clear instructions for how to unlock |
3. Gestures in automation
Mobile apps rely on gestures. When writing automated tests, you need to simulate these gestures correctly.
The four main gestures
Tap: The equivalent of a mouse click. Most basic interaction.
Long press: Hold a finger on an element for 1–2 seconds. Often reveals a context menu or additional options.
Swipe: Drag a finger across the screen in one direction. Used for scrolling, dismissing cards, navigating between pages.
Pinch and zoom: Two fingers moving apart (zoom in) or together (zoom out). Common in maps, image viewers, and PDF readers.
Why pixel coordinates are dangerous
Beginners often write gesture tests using exact pixel coordinates:
# BAD APPROACH — do not do this
driver.tap([(540, 960)]) # Tap at x=540, y=960This seems straightforward, but it breaks for several reasons:
- Different screen sizes: A Pixel 6 has a different resolution than a Galaxy S22. The button at coordinates (540, 960) on one device is in a different position on another.
- Different font sizes: If the user has large text enabled in accessibility settings, elements shift position.
- Different screen densities: The same physical button is at different pixel coordinates on different DPI screens.
- Dynamic content: If the layout changes (a banner appears, content loads), all your hardcoded coordinates are wrong.
Use accessibility IDs and semantic locators instead
The right approach is to find elements by their semantic meaning, not their position:
# GOOD APPROACH — find by accessibility ID
login_button = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "login-button")
login_button.click()
# GOOD APPROACH — find by text content
submit = driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR,
'new UiSelector().text("Login")')
submit.click()
# GOOD APPROACH — find by XPath with descriptive attribute
username_field = driver.find_element(AppiumBy.XPATH,
'//android.widget.EditText[@content-desc="email-input"]')Ask developers to add data-testid or accessibilityLabel attributes to important interactive elements. This is a common practice in teams that take testing seriously, and it makes your tests much more stable.
Swipe gestures in automation
Swipe is one of the trickier gestures to automate. The key is to swipe from one meaningful point to another, not from hardcoded coordinates:
from appium.webdriver.common.appiumby import AppiumBy
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.actions.pointer_input import PointerInput
# A simple scroll down — swipe from center-bottom to center-top
window_size = driver.get_window_size()
start_x = window_size['width'] // 2
start_y = int(window_size['height'] * 0.8)
end_y = int(window_size['height'] * 0.2)
actions = ActionChains(driver)
actions.w3c_actions.devices = [PointerInput('touch', 'finger')]
actions.w3c_actions.pointer_action.move_to_location(start_x, start_y)
actions.w3c_actions.pointer_action.pointer_down()
actions.w3c_actions.pointer_action.move_to_location(start_x, end_y)
actions.w3c_actions.pointer_action.release()
actions.perform()Notice that the scroll uses percentages of the screen size, not hardcoded pixels. This works on any screen size.
4. Permission handling in tests
Permission dialogs are a common source of test failures because they appear unexpectedly and block the test from proceeding.
How to grant permissions before tests (setup)
The best approach is to grant permissions before the test starts, not during it. This is cleaner and more reliable.
On Android, use adb to grant permissions:
# Grant camera permission before running tests
adb shell pm grant com.your.app.package android.permission.CAMERA
# Grant location permission
adb shell pm grant com.your.app.package android.permission.ACCESS_FINE_LOCATION
# Grant all dangerous permissions at once (useful for setup scripts)
adb shell pm grant com.your.app.package android.permission.CAMERA
adb shell pm grant com.your.app.package android.permission.READ_CONTACTS
adb shell pm grant com.your.app.package android.permission.RECORD_AUDIOIn Appium capabilities, you can auto-grant permissions:
desired_caps = {
"platformName": "Android",
"app": "/path/to/app.apk",
"autoGrantPermissions": True # Grant all permissions automatically
}How to test the deny scenario
To test what happens when a user denies a permission, you need to:
- Start the test with the permission NOT granted (the default for a fresh install)
- Let the permission dialog appear
- Tap "Deny" (or "Don't allow")
- Verify the app handles the denial gracefully
# When the permission dialog appears, tap "Deny"
deny_button = driver.find_element(
AppiumBy.ID, "com.android.permissioncontroller:id/permission_deny_button"
)
deny_button.click()
# Now verify the app shows a helpful message
error_message = driver.find_element(AppiumBy.ACCESSIBILITY_ID, "camera-denied-message")
assert error_message.is_displayed()How to reset permissions between runs
This is crucial for tests that check first-launch behavior. If permissions are already granted from a previous run, the permission dialog will not appear, and your test will not work correctly.
Reset permissions by clearing app data:
# Clear all app data (this resets permissions too)
adb shell pm clear com.your.app.packageOr revoke specific permissions:
# Revoke camera permission before a specific test
adb shell pm revoke com.your.app.package android.permission.CAMERAIn pytest, use a fixture to reset permissions before each test:
import pytest
import subprocess
@pytest.fixture(autouse=True)
def reset_app_permissions():
"""Reset camera permission before each test that needs it."""
subprocess.run([
"adb", "shell", "pm", "revoke",
"com.your.app.package",
"android.permission.CAMERA"
])
yield
# Cleanup after test if needed5. Creating a mobile test plan
A test plan is a document that describes what you will test, how you will test it, and in what priority order. For mobile testing, it has some specific components.
Step 1: Define your device matrix
A device matrix is a table of devices and OS versions you will run your tests on. You cannot test on every device, so you choose based on your users.
Sample device matrix for a banking app:
| Device | OS | Priority | Why |
|---|---|---|---|
| Samsung Galaxy S23 | Android 13 | P1 | Most popular Android in target market |
| Google Pixel 7 | Android 14 | P1 | Reference Android device, clean OS |
| Xiaomi Redmi Note 12 | Android 12 | P2 | Budget device, very common, tests low-end performance |
| iPhone 14 | iOS 16 | P1 | Most popular iPhone model |
| iPhone 12 | iOS 15 | P2 | Older but still widely used |
| iPad Air | iPadOS 16 | P3 | Tablet layout, lower priority |
How to determine your matrix:
- Use your app's analytics (Crashlytics, Firebase, Google Analytics)
- Check which OS versions and devices have the most users
- Include at least one budget device
- Include the latest OS + one version behind
Step 2: Assign priority levels to test scenarios
Not all tests are equally important. Assign priorities:
P1 — Critical (must pass before release):
- User registration and login
- Core user flows (place order, make payment, etc.)
- Data loss scenarios
- Security-sensitive features
P2 — Important (should pass before release):
- Secondary user flows (edit profile, change settings)
- Push notifications
- Deep links
- Permission handling
P3 — Nice to have (check when time allows):
- Edge cases and rare scenarios
- Cosmetic issues
- Orientation for less-used screens
Step 3: Decide what to automate vs what to test manually
| Test type | Automate | Manual | Notes |
|---|---|---|---|
| Critical happy paths | Yes | No | Run in CI on every build |
| Regression tests (things that used to break) | Yes | No | Prevents regressions |
| Permission flows | Yes | No | Can be scripted with adb |
| Gesture-heavy flows (complex swipes) | Sometimes | Yes | Gestures are brittle in automation |
| Visual design review | No | Yes | Requires human judgment |
| Exploratory testing (finding unknown bugs) | No | Yes | By definition cannot be automated |
| Interruptions (calls, notifications) | Partially | Yes | Hard to automate fully |
| Performance testing | Yes | Yes | Tools exist, but human observation adds value |
| Real-world conditions (subway, one hand) | No | Yes | Cannot be automated |
6. Common mobile QA mistakes beginners make
Learning from other people's mistakes is faster than learning from your own. Here are the most common errors new mobile QA engineers make:
Mistake 1: Testing only on high-end flagship devices
The problem: You test on the latest iPhone 15 Pro and Samsung Galaxy S24. Your app works perfectly. But 60% of your users have budget devices from 2021 with 3GB of RAM. The app is slow and crashes for them.
The fix: Always include at least one budget or mid-range device in your test matrix. Test on devices that represent your actual users, not the most expensive devices your company owns.
Mistake 2: Ignoring orientation changes
The problem: You test the app in portrait mode only. On Android, rotating the screen destroys and recreates the Activity, which often causes crashes or data loss. On landscape, buttons may be hidden behind the keyboard.
The fix: For every screen you test, rotate the device and confirm the layout works. Pay special attention to screens with text input.
Mistake 3: Not testing with slow network
The problem: In the office on WiFi, everything loads instantly. On a commute with poor 3G signal, the app shows a blank screen and the user does not know if anything is happening.
The fix: Test with throttled network (3G, offline). Check that: loading states are shown, errors are communicated clearly, and the app does not crash when the network is slow or absent.
Mistake 4: Hardcoding timeouts (magic numbers)
The problem: A developer writes time.sleep(3) or driver.implicitly_wait(5) in tests, assuming the app will always respond within that time. On a slow emulator or a cloud farm with high load, the test fails intermittently.
The fix: Use explicit waits that wait for a specific condition, not a specific duration:
# BAD — hardcoded timeout
time.sleep(3)
# GOOD — wait for the element to be visible
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
wait = WebDriverWait(driver, timeout=10)
element = wait.until(
EC.visibility_of_element_located((AppiumBy.ACCESSIBILITY_ID, "home-screen"))
)Mistake 5: Not resetting state between tests
The problem: Test 1 logs in. Test 2 assumes the app is on the login screen — but the user from Test 1 is still logged in. Test 2 fails.
The fix: Each test must start from a known, clean state. Use adb shell pm clear or app-level logout in your test setup to ensure a fresh start.
Mistake 6: Only testing with ideal content
The problem: You test with a normal-length name like "John Smith" and a standard profile photo. But your real users might have names in Arabic or Chinese, or upload a 20MB photo.
The fix: Test with edge case content:
- Very long names (100+ characters)
- Names with special characters and emoji
- Very large images
- Empty states (no data to show)
- Content in multiple languages (localization)
Official references
- Android — Write your tests — Official Android guide to instrumented test design
- Apple — XCTest framework — Apple's testing documentation, including UI tests and test design patterns
- W3C WebDriver Actions API — The standard that Appium uses for gesture automation, useful for understanding how swipes and taps work
Quick recap
| Topic | Key takeaway |
|---|---|
| Test types | Always cover: happy path, permissions, interruptions, network, lifecycle, orientation |
| Test case structure | Precondition → Steps → Expected result |
| Gestures | Use accessibility IDs and semantic locators, never hardcoded pixel coordinates |
| Permissions | Test grant, deny, and revoke; reset permissions between test runs with adb shell pm clear |
| Test plan | Device matrix driven by analytics, priorities P1/P2/P3, clear automate vs manual split |
| Common mistakes | High-end devices only, ignoring rotation, no slow network tests, hardcoded timeouts |
Homework
-
Write a test plan: Choose a simple app (a calculator, a notes app, or a login screen). Write a test plan that includes:
- A device matrix (at least 4 devices/OS combinations)
- At least 15 test cases in table format with preconditions, steps, and expected results
- At least 3 permission-related test cases
- At least 2 network condition test cases
- At least 2 lifecycle test cases (background/foreground)
-
Find gesture bugs: Install any app that uses swipe gestures (a dating app, a news reader, a weather app). Test swipe gestures in both portrait and landscape mode. Rotate the device while a swipe is in progress. Note any unexpected behavior.
-
Permission denial test: On any phone you own, find an app that uses the camera. Go to Settings → Apps → [App Name] → Permissions and deny the camera permission. Now open the app and try to use the camera feature. Document exactly what happens. Is the error message clear? Does the app crash? Does it offer a way to re-enable the permission?
-
Network resilience check: Open any app that loads data from the internet. Start the app, let it load some content, then immediately switch Airplane Mode on. Navigate through the app. What happens? Does it show cached data? Does it show error messages? Does it crash?
-
Device matrix research: Go to your app's analytics (if you have access) or use public data for a category of app (e.g., "top food delivery apps"). Identify the top 5 Android OS versions and top 5 iOS versions. Design a minimum viable 6-device test matrix based on this data. Write a short justification for each device choice.
Next: learn to automate these scenarios with Appium in the next track.