cswork / libraryOpen practice workspace

Articles

Detox Interview Questions: Synchronization, App State, and Flaky Mobile Tests

Practice Detox interview questions on busy resources, relaunch vs reset, scoped network exclusions, manual waits and reproducible mobile-test failures.

Detox interview questions often begin with a test that passes locally and hangs in CI. The tempting fix is another sleep. A useful answer identifies what the test waits for and whether its assumed app state survives a relaunch or a previous test.

Synchronization and isolation solve different problems. An idle app can show the wrong account. A correctly seeded app can remain busy forever because an animation never stops.

This guide uses official Detox 20.x documentation for API behavior. Its timelines, fixture designs and debugging recommendations are original exercises. The sample code requires a configured Detox app and matching test IDs; no emulator run is represented as completed here.

Detox interview questions: explain what synchronization buys you

The official synchronization troubleshooting guide explains that Detox automatically waits for tracked app activity to become idle. It also describes endlessly running animations and network activity as possible blockers. That explains why an action can wait even when its target appears on screen.

Our interview definition is practical: synchronization coordinates actions with tracked activity; it does not choose the account, create backend fixtures or guarantee that the visible screen represents the intended business result. Those remain responsibilities of the test design.

Suppose a tap on continue-button never executes. Ask whether the failure occurred before action dispatch, during element matching, or after the tap while awaiting the next state. “The button is slow” collapses those stages into one unsupported cause.

A useful opening answer is: “I would collect the busy-resource report and app logs around the blocked command, then compare them with the expected screen transition.” It gives the interviewer an investigation rather than a timeout value. If the app really has unfinished work, waiting longer may be reasonable. If the work cannot finish, the same change only makes the failure more expensive.

Read a busy-resource ledger before suppressing it

Detox documents detox test --debug-synchronization 5000 as a way to report synchronization diagnostics. The number is a debugging interval, not a sleep that makes the application ready. Check the installed version’s CLI options before incorporating it into CI.

Now examine this original, simplified ledger. These rows are teaching observations, not verbatim Detox log output.

TimeTest or app observationWhat it changes in your diagnosis
0 sApp launches into checkout; order request startsLoading may be legitimate
1 sOrder API returns 500The backend response arrived, but checkout failed
2 sApp logs show an error; loader remains animatedThe app did not complete its failure transition
5 sTest still waits for idle; no order request remainsInvestigate animation and error handling before network timeout

The likely repair in this exercise is to stop the loader and render a defined error state. Do not exclude the order API just because the screenshot contains a spinner. The recorded request has already returned.

Change the third row: the server has not answered and the order request is still in flight. Now inspect request and server evidence, including cancellation and error deadlines. Change it again: checkout is complete but a decorative loop remains active on another tab. That points to a different resource and a different mitigation.

This is how you avoid turning one symptom into a universal recipe. The official troubleshooting page even discusses off-screen or obscured animations. A screenshot tells you what is visible; a busy-resource report can reveal activity that the screenshot cannot show.

Distinguish relaunch, app reset and backend reset

The official Device API defines launchApp({newInstance: true}) as terminating and launching the app again. It separately documents resetAppState, which clears app data on Android and uses uninstall/reinstall on iOS. Restarting the process is therefore not the same operation as resetting app data.

In our original fixture, the app stores an authentication session and an unfinished cart. The backend stores the account’s orders. Test A logs in and submits an order. Test B expects the anonymous welcome screen. A process relaunch before B can leave B’s assumption wrong even if the app is perfectly idle.

Fixture boundaryExercise requirementHow to verify it
ProcessNo stale in-memory screen stateObserve initial navigation after launch
App dataIntended login and cart stateAssert anonymous or seeded-account screen
BackendKnown orders and permissionsRead the fixture account’s expected records
Device environmentDefined permission, locale and connectivity scenarioRecord configuration and system prompts

The matrix is a design asset, not a promise that one Detox method resets all four rows. Clearing app data does not reset your remote test account. Resetting a shared account can break another parallel worker.

A minimal anonymous-start example is:

beforeEach(async () => {
  await device.launchApp({resetAppState: true});
  await expect(element(by.id('welcome-screen'))).toBeVisible();
});

The example uses the documented launch option; the screen identifier is our fixture contract. For a signed-in flow, seed the backend and authenticate through a defined test path, then verify the account identity. Do not reuse a local token just because it makes the first step faster.

Ask which data the scenario is supposed to preserve. A test of cart recovery after process termination must retain the cart deliberately. Resetting everything would remove the very behavior being tested. Isolation means controlling state, not always deleting it.

Explain a bounded network exclusion

The Device API provides setURLBlacklist to exclude matching network URLs from synchronization. This changes what Detox waits for; it does not cancel those requests or make their results deterministic.

Consider our synthetic app with a foreground profile request and background analytics. The profile response determines whether the name appears. Analytics is unrelated to that assertion and may poll repeatedly. A narrow exclusion can be appropriate for the latter while leaving the profile request tracked.

await device.setURLBlacklist([
  '^https://telemetry\\.example\\.test/events(?:\\?.*)?$'
]);
try {
  await element(by.id('open-profile')).tap();
  await expect(element(by.id('profile-name'))).toHaveText('Ada');
} finally {
  await device.setURLBlacklist([]);
}

The hostname and label are original placeholders. Adapt the expression to the actual app and verify that it matches only the intended background request. A broad .* exclusion can conceal a foreground failure and leave later actions racing the network.

The official mocking guide discusses controlling dependencies for E2E tests. Our recommendation is to choose that boundary according to what the test claims to cover. A mocked profile response can test screen behavior for a specific payload. It cannot prove that the deployed profile service and authentication integration work.

In an interview, state the retained assertion after the exclusion. “The test passes with synchronization disabled” reports an outcome, not a reason to trust it. “Only analytics is excluded; the profile identity and error paths are still asserted” explains the coverage.

Use manual synchronization with a recovery condition

Detox’s Device API warns that enableSynchronization() waits for the app to become idle again. Restoring it while the same endless animation is active can block the test. The official Expect API documents waitFor(...).withTimeout(...) for bounded state expectations.

This original pattern is useful when a scenario intentionally includes transient UI and you have accepted manual coordination for a small section:

await device.disableSynchronization();
try {
  await element(by.id('show-toast')).tap();
  await waitFor(element(by.id('saved-toast')))
    .toBeVisible().withTimeout(3000);
} finally {
  await waitFor(element(by.id('saved-toast')))
    .not.toExist().withTimeout(5000);
  await device.enableSynchronization();
}

The example assumes the toast is removed from the hierarchy when it expires and that the screen then becomes idle. If your app only hides it, use the matching visibility condition. The timeout numbers are exercise choices, not recommended universal defaults.

The cleanup condition matters as much as the first assertion. If it fails, the test must fail and fixture teardown must restore the next test’s baseline. Do not swallow cleanup errors and call the run healthy. Also consider preserving the original failure separately so that a cleanup timeout does not erase the most useful diagnostic message.

Explain why manual synchronization is needed here and what makes the scope safe. If the answer is merely “CI is slower,” first measure the actual state transition. A slower device may reveal a race or a missing terminal state rather than justify bypassing idle checks across the suite.

Choose selectors and assertions that expose the failure

Official Matchers documentation recommends unique identifiers and notes that index-based matches can vary across platforms. In React Native, by.id corresponds to testID. Official Actions documentation also cautions that explicit tap coordinates depend on element geometry.

Use that behavior to improve our checkout exercise. Give the submit control a stable ID and identify an order row by its fixture identity. Matching the third text occurrence of “Order” can select a different row after sorting, localization or pagination.

Then separate existence, visibility and meaning. An order-summary component can exist behind a loading overlay. A visible summary can belong to a previous account. A successful tap does not prove that the server accepted the intended order. Assert the specific result that the test is supposed to establish.

For example, capture the fixture order identifier and check that the screen shows its status. If testing duplicate submission prevention, inspect the backend fixture record count as well as the UI. Keep those assertions tied to the case; adding unrelated checks makes a failure harder to explain.

When a matcher fails, inspect the hierarchy and the app state before changing the selector. The element might correctly be absent because navigation failed. Replacing a stable ID with a coordinate can turn that useful failure into a tap on the wrong screen.

Reproduce CI flakiness as a controlled comparison

Detox’s flakiness guide describes tests whose outcomes vary without an app change, including machine-dependent failures. Our recommended investigation records the build, Detox version, platform, device configuration, fixture identity, test order and first failing command.

Run the suspect test alone with a known baseline, then run its predecessor followed by the suspect test. In the anonymous-screen example, failure only after the login test is evidence for state coupling. Next keep the sequence fixed and change one reset boundary. That is more informative than simultaneously changing timeouts, selectors and concurrency.

For the checkout case, retain the server response, app error transition, busy-resource report and screenshot on the first failure. A rerun may hide the original evidence. Report how many attempts failed and under which condition instead of announcing that a handful of passing retries proves the problem solved.

The final interview answer should name a falsifiable cause: “The error response leaves the loader running,” or “Test B inherits Test A’s stored session.” Then explain the repair and the counterexample that would reject your diagnosis. You can do this reasoning from supplied logs even when an interviewer does not provide a working mobile project.

Sources and Further Reading