How to Write Test Cases for Background Sync (With Examples)
How to Write Test Cases for Background Sync (With Examples)
How to Write Test Cases for Background Sync (With Examples)
How to Write Test Cases for Background Sync (With Examples) – Test Case Anatomy
A test case for background sync must capture the asynchronous nature of the operation, the conditions that trigger it, and the observable outcomes once the sync completes or fails. Begin by identifying the trigger (e.g., network change, periodic interval, user action), the worker that performs the sync (such as Android’s WorkManager, iOS BackgroundTasks, or a Service Worker), and the observable effect (data persisted, UI updated, notification shown, or error logged).
Each test case consists of four essential parts:
- Identifier – a short, unique code (TC‑BS‑001) that enables traceability to requirements and test‑management tools.
- Preconditions – the device state, app state, and external conditions required before execution (e.g., app in foreground, network disabled, account logged in).
- Steps – the precise actions a tester or automation script performs to initiate the sync and to observe the result. Steps should be deterministic; avoid reliance on timing unless you‑on‑random delays.
- Expected Result – the verifiable outcome after the sync finishes, expressed as a pass/fail criterion (e.g., “a row with ID 123 appears in the local SQLite table”, “a toast with text ‘Sync completed’ is shown”, “the network layer receives a POST to /sync with HTTP 200”).
When writing the steps, separate the setup (putting the app into the precondition state) from the execution (the action that starts the sync) and the verification (checks performed after a defined wait or event). For background sync, the verification often relies on callbacks, broadcast receivers, or polling a known state change with a timeout.
Document any assumptions (e.g., device API level ≥ 21, battery not in low‑power mode) and dependencies (specific versions of WorkManager, server API contract). These notes help reviewers understand why a test may be flaky on certain configurations and guide maintenance when the underlying platform changes.
Finally, attach a requirement ID (e.g., REQ‑BS‑07) to each test case. This creates a traceability matrix that shows coverage of functional specifications and highlights gaps when new features are added.
How to Write Test Cases for Background Sync (With Examples) – Positive Test Cases
Positive test cases validate that the background sync works as intended under normal conditions. They confirm that the trigger correctly schedules the worker, the worker completes its work, and the system updates the expected state.
TC‑BS‑001 – Sync on Network Restoration
Preconditions
- App is logged in with a valid user token.
- Device is connected to Wi‑Fi.
- No pending sync items in the local queue.
- WorkManager is initialized with a
PeriodicWorkRequestset to 15 minutes and aOneTimeWorkRequestconstrained toNetworkType.CONNECTED.
Steps
- Disable Wi‑Fi (set airplane mode ON, then OFF to ensure a clean state).
- Force the app to create a sync item by tapping “Add Item” in the UI (this inserts a row into the local pending‑sync table).
- Verify that the row exists in the local table (SELECT * FROM pending_sync WHERE processed = 0).
- Enable Wi‑Fi and wait for the system to detect network change.
- Observe the device log for WorkManager start (
WM-WorkerFactory: Creating worker SyncWorker). - After the worker finishes (max wait 30 seconds), check the local table again.
Expected Result
- The row previously in
pending_syncis either deleted or markedprocessed = 1. - A corresponding entry appears in the remote server (verified via mock server log or API call count).
- No error is logged in Logcat for the worker.
TC‑BS‑002 – Periodic Sync Triggers at Interval
Preconditions
- App is in the background (user switched to home screen).
- Device is plugged in and battery level > 80 % (to satisfy
setRequiresBatteryNotLow). - Network is available (Wi‑Fi ON).
- A
PeriodicWorkRequestwith repeat interval 1 hour is enqueued at app start.
Steps
- Clear any existing work with
WorkManager.getInstance().cancelAllWork(). - Launch the app, which enqueues the periodic worker.
- Immediately query WorkManager for pending work (
getWorkInfosByTagLiveData("periodic_sync")) and confirm oneENQUEUEDentry. - Advance the system clock by 1 hour + 10 seconds using
adb shell cmd clock set. - Wait for the worker to start (listen for
Worker: SyncWorker started). - After completion, verify that the worker performed its intended work (e.g., fetched latest configuration from server and updated local SharedPreferences).
Expected Result
- The worker runs exactly once after the clock jump.
- Local configuration matches the server’s latest version (checked via a getter method).
- No duplicate
ENQUEUEDwork remains (the periodic request is rescheduled automatically).
TC‑BS‑003 – Sync Initiated by User Action (Pull‑to‑Refresh)
Preconditions
- App is in foreground, showing a list of items fetched from server.
- Network is available.
- Swipe‑to‑refresh gesture is enabled and bound to a
WorkRequestwith constraintsNetworkType.CONNECTED.
Steps
- Perform a downward swipe on the list (simulate via Espresso
swipeDown()). - Observe the progress spinner appears.
- Wait for the spinner to disappear (max 15 seconds).
- Check that the list now contains any server‑side changes made during the test (e.g., a new item added via API).
Expected Result
- The refresh gesture triggers a OneTimeWorkRequest that completes successfully.
- The UI updates to reflect the latest server state without manual restart.
- No error toast or snackbar appears.
These three cases illustrate the core positive scenarios: network‑driven, time‑driven, and user‑driven triggers. Each follows the anatomy defined earlier, with clear preconditions, deterministic steps, and measurable expected results.
How to Write Test Cases for Background Sync (With Examples) – Negative Test Cases
Negative test cases verify that the sync behaves correctly when something goes wrong or when conditions prevent the work from starting. They ensure graceful degradation, proper error handling, and that the system does not corrupt data.
TC‑BS‑004 – Sync Disabled When Battery Saver Is On
Preconditions
- Device battery saver mode is enabled (Settings → Battery → Battery saver → Turn on now).
- App has a
WorkRequestconstrained withsetRequiresBatteryNotLow(true).
Steps
- Ensure the app is in the foreground.
- Trigger a sync condition (e.g., add a pending‑sync item).
- Wait for a period longer than the worker’s expected delay (30 seconds).
- Query WorkManager for the work state (
getWorkInfoByIdLiveData).
Expected Result
- The work remains in
ENQUEUEDstate; it does not transition toRUNNING. - No worker execution logs appear.
- After disabling battery saver, the work automatically starts (verifies resumption).
TC‑BS‑005 – Sync Fails Due to Server Error (500)
Preconditions
- Mock server configured to return HTTP 500 for the
/syncendpoint. - App has a pending sync item ready.
- Network is available.
Steps
- Enable network and ensure the app is in background.
- Allow the sync worker to run (trigger via network change or manual
WorkManager.enqueue). - Capture the worker’s
Result.retry()call orResult.failure()via a test listener. - After the worker finishes, inspect the local pending‑sync table.
Expected Result
- The worker returns
Result.retry()(if a retry policy is defined) orResult.failure(). - The pending‑sync item remains unprocessed (still
processed = 0). - An error is logged (
SyncWorker: Sync failed with status 500). - No data is corrupted; the local item is unchanged.
TC‑BS‑006 – Sync Skipped When Storage Is Full
Preconditions
- Device storage is > 95 % full (use
adb shell pm set-install-location 2and fill with large files). - App attempts to write a sync result to a local file or database.
Steps
- Trigger a sync that would normally write data (e.g., download a payload and store it).
- Observe the worker’s execution.
Expected Result
- The worker detects the storage shortage (checks
StatFsfor free space) and returnsResult.retry()with a back‑off delay. - No partial write is left in the file system.
- A warning is logged (
SyncWorker: Insufficient storage, deferring work).
TC‑BS‑007 – Duplicate Sync Requests Are Coalesced
Preconditions
- WorkManager is configured with
ExistingWorkPolicy.KEEPfor a unique work name. - Network is available.
Steps
- Enqueue the same
OneTimeWorkRequest(named “sync_job”) five times in rapid succession. - Immediately query WorkManager for all works with that name.
Expected Result
- Only one work instance appears in the
ENQUEUEDstate. - The subsequent four enqueue calls are ignored (no additional work objects).
- When the work runs, it executes exactly once.
These negative cases confirm that the sync logic respects system constraints, handles server failures gracefully, avoids data loss under resource pressure, and prevents unnecessary work duplication.
How to Write Test Cases for Background Sync (With Examples) – Edge and Boundary Cases
Edge cases push the system to its limits, while boundary cases test values just inside and just outside accepted ranges. For background sync, relevant dimensions include timing intervals, payload sizes, retry counts, and concurrency.
TC‑BS‑008 – Minimum Interval Enforced by OS
Preconditions
- Android 12+ (where
setMinimumIntervalis enforced at 15 minutes for periodic work). - App attempts to schedule a periodic worker with interval 1 minute.
Steps
- Enqueue a
PeriodicWorkRequestwithsetPeriodic(1, TimeUnit.MINUTES). - Immediately query the work info.
Expected Result
- The actual interval stored in the work request is adjusted to 15 minutes (or the system‑defined minimum).
- No exception is thrown; the work is accepted.
- A debug log shows
PeriodicWorkRequest: Interval adjusted to 15 minutes.
TC‑BS‑009 – Maximum Payload Size for Sync
Preconditions
- Mock server limits the sync payload to 100 KB; larger payloads are rejected with HTTP 413.
- App prepares a sync item containing a base‑64‑encoded image.
Steps
- Create a sync item with a 90 KB payload (under limit).
- Trigger sync and verify success.
- Create a second sync item with a 120 KB payload (over limit).
- Trigger sync and observe the outcome.
Expected Result
- The 90 KB item syncs successfully; server logs show receipt.
- The 120 KB item triggers a
Result.failure()(or retry if policy allows) and logs an error about payload too large. - The local pending‑sync item for the oversized payload remains unprocessed.
TC‑BS‑010 – Maximum Retry Count Exceeded
Preconditions
- WorkRequest is built with
setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)andsetRetryLimit(3). - Mock server always returns HTTP 503.
Steps
- Enqueue the work request.
- Allow the worker to run until it exhausts retries (approximately 10 + 20 + 40 + 80 seconds).
- Check the final work state.
Expected Result
- After the fourth attempt, the work transitions to
FAILED. - No further retries are scheduled.
- The pending‑sync item remains unchanged, and a final error log is recorded (
SyncWorker: Max retries exceeded).
TC‑BS‑011 – Concurrency: Two Workers Attempt to Update Same Record
Preconditions
- App uses a Room database with a
@Daomethod that updates a row without transaction protection. - Two identical
OneTimeWorkRequestinstances are enqueued simultaneously (different work IDs but same payload).
Steps
- Insert a record with version = 1.
- Enqueue Worker A and Worker B at the same time (using
WorkManager.enqueueUniqueWorkwithExistingWorkPolicy.REPLACEto avoid OS coalescing, but schedule them with a 0‑ms offset via a custom executor). - Each worker reads the record, increments version, and writes back.
Expected Result
- Because the DAO lacks a transaction, a race condition may cause the final version to be 2 instead of 3 (lost update).
- The test should detect this and flag the need for a
@Transactionannotation or use ofRoomDatabase.runInTransaction. - If the code already uses a transaction, the final version will be 3, confirming correctness.
TC‑BS‑012 – Sync Triggered Immediately After Device Boot
Preconditions
- Device is powered off.
- App has set
setInitialDelay(0, TimeUnit.SECONDS)and addedsetRequiredNetworkType(NetworkType.CONNECTED)to aOneTimeWorkRequest. - A
BroadcastReceiverforBOOT_COMPLETEDforwards the request to WorkManager.
Steps
- Power on the device and wait for boot to complete (monitor
adb shell getprop sys.boot_completed). - Immediately after boot, check for network availability (simulate Wi‑Fi ON via script).
- Observe whether the worker starts.
Expected Result
- If network is available at boot, the worker runs within the OS‑defined window (typically within a few seconds).
- If network is not yet available, the work stays
ENQUEUEDuntil network appears, then runs. - No crash or ANR occurs during boot.
TC‑BS‑013 – Sync During Screen Rotation (Configuration Change)
Preconditions
- App retains a reference to a
ViewModelthat observes LiveData from the worker’s output. - Device is in portrait orientation.
Steps
- Trigger a sync that will take ~5 seconds (simulate with
Worker.doWork()sleeping). - While the worker is running, rotate the device to landscape (via
adb shell content insert --uri content://settings/system --name "accelerometer_rotation" --value 1then change orientation). - Observe the UI and LiveData after rotation.
Expected Result
- The ViewModel survives the configuration change; LiveData continues to emit the worker’s result.
- No duplicate work is started due to the rotation.
- UI updates correctly once the worker finishes, showing the synced data.
These edge and boundary cases expose timing limits, payload constraints, retry policies, concurrency hazards, boot‑time behavior, and configuration‑change resilience. Including them ensures the sync mechanism remains robust under extreme or uncommon conditions.
How to Write Test Cases for Background Sync (With Examples) – Test Data Setup and Management
Reliable background‑sync tests depend on reproducible data states. Flaky tests often arise from leftover files, shared preferences, or database rows that persist across runs. A solid data‑management strategy consists of three layers: environment preparation, test‑specific data injection, and post‑test cleanup.
Environment Preparation
Start each test suite with a clean device state:
# Reset app data and clear WorkManager’s internal queue
adb shell pm clear com.example.myapp
adb shell cmd job reset
# Optionally put the device in a known power state
adb shell dumpsys battery set ac 1
adb shell dumpsys battery set level 100
Clearing the app’s private directory removes databases, SharedPreferences, and file caches. Resetting the job scheduler ensures that any pending work from previous runs does not interfere.
Test‑Specific Data Injection
For each test case, create the minimal dataset required to reach the precondition. Use helper functions that operate directly on the app’s ContentProvider or Room database, bypassing UI where possible for speed.
fun insertPendingSyncItem(id: Long, payload: String): Long {
val values = ContentValues().apply {
put(PendingSyncContract.Columns.ID, id)
put(PendingSyncContract.Columns.PAYLOAD, payload)
put(PendingSyncContract.Columns.PROCESSED, 0)
}
return appContentResolver.insert(PendingSyncContract.CONTENT_URI, values)!!
}
If the app uses a Dependency Injection framework (e.g., Hilt), provide a test module that swaps the real repository with an in‑memory fake. This allows you to set flags like networkAvailable = false or batteryLow = true without toggling system settings.
Post‑Test Cleanup
After verification, delete any test‑inserted rows and clear in‑memory caches. A teardown method can be written in JUnit 5 as:
@AfterEach
fun tearDown() {
appContentResolver.delete(PendingSyncContract.CONTENT_URI, null, null)
WorkManager.getInstance(appContext).cancelAllWorkByTag("test_sync")
// Clear any mock server expectations
mockServer.reset()
}
Data Variability Strategies
To increase confidence, parametrize tests with a range of values:
| Variable | Values Tested | Purpose |
|---|---|---|
| Payload size (KB) | 0, 1, 10, 50, 90, 100, 110, 200 | Verify boundary handling of limits |
| Retry count | 0, 1, 2, 3, 4 | Confirm back‑off and max‑retry behavior |
| Network latency | 0 ms, 50 ms, 200 ms, 1000 ms (via tc qdisc) | Test timeout and retry policies |
| Battery level | 5 %, 20 %, 50 %, 90 % | Ensure battery‑saver constraints work |
Running the same test logic across this matrix (often via @ParameterizedTest in JUnit 5) uncovers issues that a single‑value test would miss.
Mocking External Dependencies
Background sync frequently contacts a remote endpoint. Use a programmable mock server (e.g., WireMock, MockWebServer) to simulate:
- Success responses (200, 201) with varying body sizes.
- Error responses (400, 401, 429, 500, 503).
- Delayed responses to test timeouts.
- Malformed JSON to validate parsing guards.
Example WireMock stub for a failing sync:
stubFor(post(urlEqualTo("/sync"))
.willReturn(aResponse()
.withStatus(500)
.withFixedDelay(1200) // simulates server lag
.withHeader("Content-Type", "application/json")
.withBody("{\"error\":\"internal\"}")));
By controlling the server, you can reproduce deterministic outcomes for negative and edge cases without relying on flaky network conditions.
How to Write Test Cases for Background Sync (With Examples) – Prioritization and Traceability
Not all test cases carry equal risk. Prioritization helps focus limited testing effort on the scenarios most likely to cause user‑visible failures. A common approach combines impact (how severe the consequence if the test fails) and likelihood (how often the condition occurs in production).
Impact‑Likelihood Matrix
| Priority | Impact (User‑visible) | Likelihood (Occurrence) | Example Test Cases |
|---|---|---|---|
| P0 | Crash, data loss, security breach | High (daily) | TC‑BS‑004 (Battery saver blocks sync – could cause missed critical updates) |
| P1 | UI inconsistency, missed notification | Medium (weekly) | TC‑BS‑002 (Periodic sync interval) |
| P2 | Minor performance degradation, log spam | Low (monthly) | TC‑BS‑009 (Payload size limit) |
| P3 | Cosmetic, edge‑case only | Very low (rare) | TC‑BS‑012 (Boot‑time sync) |
Assign each test case a priority based on where it falls in the matrix. During a sprint, aim to execute all P0 and P1 cases on every build, P2 on nightly runs, and P3 on weekly or pre‑release cycles.
Traceability to Requirements
Link each test case to the requirement(s) it validates. Use a simple two‑column table in your test‑management tool or a markdown file:
| Test Case ID | Requirement ID(s) | Description |
|---|---|---|
| TC‑BS‑001 | REQ‑BS‑07, REQ‑BS‑12 | Sync initiates when network becomes available after being offline. |
| TC‑BS‑004 | REQ‑BS‑03 | Sync is deferred when Battery Saver is active to preserve power. |
| TC‑BS‑009 | REQ‑BS‑15 | Payloads exceeding the server limit are rejected and retried according to policy. |
| TC‑BS‑011 | REQ‑BS‑09, REQ‑BS‑10 | Concurrent workers updating the same record do not cause lost updates (transactional safety). |
Maintaining this matrix enables impact analysis when a requirement changes: you can instantly identify which test cases need review, addition, or retirement.
Risk‑Based Test Selection
When time is constrained, apply a risk‑based filter:
- Select all P0 tests.
- Add P1 tests that touch modified code (use
git diffto identify changed modules and map them to test cases via the traceability table). - If capacity remains, include a random 20 % of P2 tests to catch regressions in less‑critical areas.
- Schedule P3 tests for a dedicated “exploratory” sprint where autonomous tools can exercise them without manual effort.
This approach ensures that the most critical paths are always verified while still providing coverage for lower‑risk functionality over longer cycles.
How to Write Test Cases for Background Sync (With Examples) – Manual vs Automated Execution Strategies
Background sync lends itself to both manual exploratory testing and automated regression suites. Each approach has strengths, and combining them yields the highest confidence.
Manual Testing Strengths
- Exploratory flexibility: Testers can simulate realistic user interruptions (incoming calls, rapid orientation changes, switching apps) that are difficult to script deterministically.
- Rapid feedback on UI: Visual cues such as toasts, snackbars, or progress indicators are instantly noticeable.
- Ad‑hoc environment manipulation: Using device settings UI to toggle airplane mode, battery saver, or network type is quicker than scripting ADB commands for a one‑off check.
Manual checklist for a sync feature (run after each build):
- Verify that the app shows a sync‑in‑progress indicator when a background job starts.
- Confirm that turning off Wi‑Fi while a sync is running pauses the job and resumes when connectivity returns.
- Ensure that error states display a user‑friendly message (not a raw stack trace).
- Check that the app does not crash when the device is low on storage and a sync attempts to write a large file.
- Validate that the sync respects Do Not Disturb mode (no audible notifications unless marked high priority).
Automated Testing Foundations
Automation excels at repeatability, data‑driven variations, and continuous integration. For background sync, focus on:
- WorkManager verification: Use
TestListenableFutureorCountingTaskExecutorto synchronously execute workers and assert outcomes without relying on real time. - UI‑agnostic assertions: Check the source of truth (database, SharedPreferences, file system) rather than UI elements that may change.
- Deterministic timing: Leverage
TestWorkerFactoryandSynchronousExecutorto eliminate flaky waits.
Example automated test using AndroidX Test and WorkManager’s testing library:
@RunWith(AndroidJUnit4::class)
class SyncWorkerTest {
private lateinit var context: Context
private lateinit var worker: SyncWorker
private lateinit var testExecutor: TestExecutor
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
testExecutor = TestExecutor()
worker = SyncWorker(
context,
WorkerParameters.Builder()
.setExistedInputData(workDataOf("payload" to "test"))
.build(),
testExecutor
)
}
@Test
fun `worker returns success when mock server responds 200`() {
// Given
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
// When
val result = worker.doWork()
// Then
assertThat(result).isEqualTo(Result.success())
// Verify local DB updated
val db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
val item = db.syncDao().getLatest()
assertThat(item.processed).isTrue()
}
}
This test runs instantly on the JVM, validates the worker’s logic, and isolates external dependencies via MockWebServer.
Hybrid Approach with Autonomous Exploration
Tools
SUSAUSATest (the app by executing background sync flows. It creates regression scripts (Appium for Android, Playwright for Web) from the paths it discovers. While SUSA is not a replacement for hand‑crafted test cases, it can surface scenarios that a tester might not think of, such as:
- A specific sequence of background sync followed by a foreground navigation that triggers a race condition.
- An edge case where a notification is posted while the device is in Do Not Disturb, causing the system to defer the sync.
When you integrate SUSA into your CI pipeline, you can:
- Run an exploratory pass on a nightly build to collect new paths.
- Export the generated Appium/Playwright scripts and add them to your automated suite as supplemental regression checks.
- Review the discovered paths for gaps in your manual test matrix and add corresponding test cases.
This combination leverages the repeatability of scripted automation and the creativity of autonomous exploration, delivering broader coverage of background‑sync behavior.
How to Write Test Cases for Background Sync (With Examples) – Checklist and Takeaways
Before you consider a background‑sync feature “tested,” run through the following concise checklist. Each item corresponds to a pattern observed in the test cases above.
Background‑Sync Test Checklist
| ✅ Item | Why It Matters |
|---|---|
| Trigger coverage – at least one test for each trigger type (network change, periodic timer, user action, boot, content‑provider change). | Guarantees the scheduler reacts to all possible start conditions. |
| Constraint validation – verify each constraint (network type, battery not low, storage, charging) prevents work when false and allows it when true. | Prevents battery drain, data overage, or work running in impossible states. |
| Success path – confirm that a successful sync updates the source of truth (DB, file, preferences) and produces the expected user‑visible outcome (toast, UI update, notification). | Core functional correctness. |
Error handling – test server error codes (4xx, 5xx), network timeouts, and malformed responses; ensure the worker returns appropriate Result and does not corrupt local state. | Guarantees graceful degradation and retry logic. |
| Retry and back‑off – validate that the back‑off policy respects the configured delay and max attempts, and that work is eventually retried or fails permanently. | Avoids tight loops that waste CPU and battery. |
| Concurrency safety – if multiple workers can touch the same data, ensure transactions or locking prevent lost updates. | Prevents data corruption under load. |
| Resource limits – test maximum payload size, minimum/maximum interval, and low‑storage conditions. | Ensures the app behaves within platform‑imposed bounds. |
| Lifecycle resilience – confirm work survives process kill, device reboot, and configuration changes (screen rotation, language switch). | Guarantees reliability across real‑world usage. |
| Observability – check that logs, metrics, or WorkManager status expose the sync state for monitoring in production. | Enables post‑release debugging and alerting. |
| Cleanup – after each test, clear test‑inserted data and cancel any leftover work to avoid cross‑test contamination. | Maintains test suite stability. |
Key Takeaways
- Start with the trigger‑constraint‑outcome triad. Every background‑sync test can
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