Test Levels and the Test Pyramid Questions
How unit, integration, component, end-to-end and contract tests fit together and where each provides the most value. Covers the test pyramid and the competing shapes proposed against it (the testing trophy and the honeycomb), multi-layer test architecture, contract testing as the seam between services, choosing the right level to catch a given class of defect cheaply, and what to run per commit versus per release. Includes the cost and confidence trade-offs between fast low-level tests and slower, broader system tests. The scope is which level a test belongs at and why. Deciding how much to invest in testing and where to prioritize under time pressure is covered separately.
Tell me about a time you advocated for improving testing practices on a mobile engineering team. Describe the problems you observed, the changes you proposed (tools, process, or refactoring), how you prioritized and implemented them, the metrics you used to measure success, and how you handled resistance from teammates or product managers.
Sample Answer
A strong answer to this question demonstrates that you correctly diagnosed a SPECIFIC, mobile-particular testing gap, not a generic "we needed more tests" observation, and that you drove the change with evidence rather than just opinion.
How to structure the STAR response
Situation and problems observed: describe a concrete, mobile-specific gap, for example a team that had solid backend test coverage but almost no automated coverage on the mobile client itself, relying instead on manual QA passes before each release, which was becoming a release-cadence bottleneck as the app grew, or a team whose existing mobile UI tests were so flaky that they were routinely ignored, effectively providing zero real signal despite real engineering time spent maintaining them.
Changes you proposed: be specific about WHICH level and WHY, for example proposing a foundational layer of unit tests for the app's view-model and business-logic layer first (highest value per hour invested, since almost none currently existed), rather than jumping straight to UI test automation, or proposing a specific fix to the flaky UI suite's root cause (unstable selectors, missing explicit waits for async work) rather than simply asking for "more reliable tests" in the abstract.
How you prioritized and implemented them: describe a realistic, incremental rollout, for example starting with the highest-traffic, highest-risk screens first (login, checkout) as a pilot to demonstrate value before asking for broader team buy-in, and pairing the technical change with a process change (a definition-of-done requirement for new features to include unit tests) so the improvement didn't erode again once the initial push ended.
Metrics used to measure success: use metrics you can honestly derive from the situation, such as the manual QA cycle time before and after (if it's a release-bottleneck story), or the automated suite's pass-rate stability and rerun rate before and after (if it's a flakiness story), described honestly and specifically rather than with an invented precision figure; if you don't have an exact number from memory, describe the direction and rough magnitude of the change honestly (for example, manual QA time dropping from most of a release cycle to a small fraction of it) rather than fabricating a specific percentage.
Handling resistance: name a real, specific form of resistance and how you addressed it with evidence rather than authority, for example a teammate skeptical that unit-testing view-model logic was worth the time investment, addressed by pointing to a specific recent bug that a proposed test would have caught, or a product manager concerned that adding tests would slow down an already tight release schedule, addressed by showing that the pilot screens' review and QA time actually DECREASED once automated coverage existed, turning the argument from "tests slow us down" to "tests are what let us go faster."
Trade-offs and pitfalls
The pitfall in this story is claiming credit for a purely technical fix without acknowledging the process and buy-in work required to make it stick; testing improvements that aren't paired with a process change (definition of done, code-review expectations) tend to erode once the person who pushed for them moves on, and naming that explicitly is itself a sign of senior-level thinking about sustainable change, not just a one-time fix.
For a cross-platform codebase (for example, Flutter or React Native) that includes some native modules, propose how to allocate testing responsibility: which logic to cover with unit tests in the shared code, which native-specific features require per-platform tests, and how you would combine native UI tests with cross-platform end-to-end tests to minimize duplicated effort while maximizing coverage.
Sample Answer
A cross-platform codebase with native modules has three genuinely distinct code populations, and the right test allocation follows directly from which population each piece of logic lives in.
Shared code: unit tests
Any logic that lives in the shared cross-platform layer (business rules, state management, data transformation, API-client logic) should be unit-tested ONCE, in the shared layer itself, using the cross-platform framework's own testing tools (for example Dart tests for Flutter, or Jest for React Native). This is the highest-leverage layer to invest in, since a bug found and fixed here is fixed for every platform simultaneously, and a test written here verifies every platform at once rather than needing platform-specific duplicates.
Native-specific features: per-platform tests
Any feature that genuinely requires native code, camera access, platform-specific biometrics, deep OS integration, cannot be verified by the shared layer's tests at all, since the actual behavior under test lives entirely in platform-specific code the cross-platform framework doesn't execute. These need dedicated per-platform tests: XCTest for the iOS-native module, an equivalent JVM/Android test for the Android-native module, each verifying that platform's actual implementation of the native feature.
Combining native UI tests with cross-platform end-to-end tests
For the small number of end-to-end journeys that matter most, use the cross-platform framework's own end-to-end tooling, and note these tools don't cross over between frameworks: Detox for React Native, or Flutter's official integration_test package for Flutter (Patrol is a common third-party alternative when a test needs to interact with native permission dialogs or other platform UI outside the Flutter widget tree). Either way, drive the FULL app, including native modules, on both platforms of whichever framework you're on, rather than writing separate native-only end-to-end suites for iOS and Android; this avoids the duplicated effort of maintaining two parallel end-to-end suites for logic that's actually shared. Reserve native-only UI tests for verifying a native module's OWN platform-specific interaction details in isolation (does a native camera permission prompt display correctly on this specific OS version), which the cross-platform end-to-end tooling either can't reliably exercise or can't do so with enough platform-specific precision.
Minimizing duplication while maximizing coverage
The discipline that makes this work is asking, for every piece of logic, "does this code path exist identically on both platforms, or does it diverge?" Shared logic gets exactly one test, in the shared layer; genuinely divergent native logic gets exactly one test per platform where it diverges, never both a shared-layer test AND a redundant per-platform test for the same logic. The most common source of wasted effort is testing the SAME shared business logic redundantly inside each platform's native test suite "just to be sure," which adds maintenance cost without adding real coverage, since the shared-layer test already proves that logic correct on every platform that consumes it.
Trade-offs and pitfalls
The trap specific to cross-platform codebases is assuming the shared layer's tests provide FULL coverage of a feature just because most of its logic lives there, when a feature that touches even one native-specific interaction point (a permission prompt, a platform gesture) needs a dedicated native-level test for that specific interaction, layered on top of, not instead of, the shared-layer coverage for the rest of the feature's logic.
Your mobile product uses feature flags extensively. Design a testing strategy that validates feature-flagged code paths across unit tests, staged-rollout validation, and end-to-end permutations, in continuous integration, without exploding the total number of tests. Discuss API-driven flag control for tests and safeguards to prevent false positives or false negatives caused by the flags themselves.
Sample Answer
Feature flags multiply a mobile app's effective state space (every flag combination is a distinct version of the app), so a naive "test every combination at every level" approach explodes combinatorially; the strategy has to deliberately choose which level handles which part of that explosion.
Unit tests: the flag-evaluation logic itself
Test the function that DECIDES a flag's effective value (given the stored flag state, any user segment, any override) in complete isolation, covering on, off, and each override case directly, with no UI or network involved. This is where you can afford to test every meaningful combination of INPUTS to the decision logic, since it's cheap; it is not where you test every combination of flags interacting with the rest of the app, since that's a different, much larger problem handled at other levels.
Staged-rollout validation
Separately from the per-flag decision logic, test the ROLLOUT MECHANISM itself: given a configured rollout percentage, does the flag system correctly and consistently assign a given user or device to the flagged or unflagged group, and does that assignment stay stable across app launches (a user shouldn't flip between variants on every relaunch). This is a narrow, dedicated test of the rollout infrastructure, not of every feature that happens to use it.
End-to-end permutations, without exploding test count
Rather than testing every combination of every flag end-to-end, test each flag's ON and OFF state independently against the app's DEFAULT configuration for every other flag, plus a small, deliberately chosen set of specific combinations known to interact (two flags that touch the same screen or the same underlying data), identified by reviewing which flags share code paths rather than guessing. This keeps end-to-end test count linear in the number of flags rather than exponential, at the accepted cost of not exercising every theoretical combination, which is a reasonable trade given that most flag pairs genuinely don't interact at all.
API-driven flag control for tests
Expose an API or test-only override mechanism that lets automated tests set a specific flag state directly for a test run, rather than relying on the same staged-rollout percentage mechanism real users go through; this makes end-to-end tests deterministic (a test can force a flag ON or OFF reliably) instead of depending on random assignment, which would make the same test flaky from run to run.
Safeguards against false positives and false negatives from the flags themselves
A false negative (a real bug hidden because a test always ran with a flag in one particular state) is guarded against by ensuring the test suite explicitly exercises both states for every flag that affects a tested code path, not just whatever the default happens to be. A false positive (a test failing only because of unrelated flag state, not because of a real bug) is guarded against by having tests explicitly declare which flag states they require rather than implicitly depending on whatever the test environment's defaults happen to be, so a change to an unrelated flag's default doesn't silently break unrelated tests.
Trade-offs and pitfalls
The pitfall specific to feature flags is treating "we have end-to-end coverage" as meaning "we've tested every real configuration users will see," when in practice a permutation-limited approach, by design, doesn't cover every combination; keep an inventory of currently-active flags and periodically review whether any pair now shares enough code to warrant adding a dedicated combination test, since flag interactions can emerge over time even when they didn't exist when each flag was first introduced.
Explain how contract testing (for example, using Pact) can be used for a mobile client that consumes a REST API. Describe where the consumer's contracts should live, how provider validation works in the backend team's CI, and the trade-offs compared to relying on full end-to-end integration tests against a real or staged backend.
Sample Answer
A mobile client is always the CONSUMER side of a contract with its backend API, which shapes how contract testing should be set up: the mobile team owns declaring what it needs, and the backend team owns proving they still provide it, without either side needing the other's real system running to verify their half.
Where the consumer's contracts should live
The mobile app's contract, generated from tests the mobile team writes expressing exactly what shape of request and response it depends on, should be published to a shared location both teams' CI can reach, most commonly a contract broker (such as a Pact Broker) rather than living only inside the mobile repository. Keeping it in a shared broker is what lets the backend team discover, automatically, every contract they need to satisfy across every consumer (this mobile app, possibly a web client, possibly other services), rather than the mobile team having to manually notify the backend team of its expectations.
How provider validation works in the backend team's CI
On every change to the backend API, the backend team's CI pulls the latest published contracts from the broker (including the mobile client's) and replays each one against the REAL backend implementation, confirming the real responses still satisfy every consumer's stated expectations. This runs entirely within the backend team's own CI, at backend-test speed, with no mobile app, emulator, or device involved at all; a violation is caught the moment the backend team makes a breaking change, before it ever reaches a shared or staging environment the mobile team might be testing against.
Trade-offs compared to full end-to-end integration tests
A full end-to-end test (the real mobile app against a real or staged backend) proves the ENTIRE path, including things a contract test cannot see: real network behavior, real serialization over the wire, and real UI behavior in response to a real response. But it is slow, requires both a running mobile build and a running (or staged) backend, and when it fails, doesn't immediately tell you which side is at fault. A contract test is dramatically faster (each side runs independently, no device or full backend deployment needed) and immediately attributes a failure to a specific side, but it is only as good as the fidelity of the contract itself: a contract that omits a real constraint the mobile app depends on (a specific error-response shape it needs to display correctly) won't catch a break in that specific behavior, which only a broader integration or end-to-end test would surface.
Trade-offs and pitfalls
The practical answer is not "contract tests instead of end-to-end tests" but "contract tests for the bulk of PR-time confidence, with a small, curated end-to-end layer retained for the handful of mobile journeys where proving the real device, real network, and real backend genuinely work together end-to-end is worth the cost," mirroring the general pyramid principle of reserving the most expensive level for what only it can prove.
Design an integration-testing approach for a mobile app that uses a local database (such as Room or SQLite), a REST API backend, and a background sync worker. Describe which layers you would test together, how you would simulate backend behavior (stubs, mocks, or a test server), how you would validate resulting database state and background work, and how to keep these tests fast and reliable in CI.
Sample Answer
A mobile app with a local database, a REST backend, and a background sync worker has three real integration points, and the right approach tests each SEAM deliberately rather than trying to integrate everything at once.
Which layers to test together
Test the local-database-plus-repository layer together, against a real (in-memory or on-device test) instance of the local database itself, Room on Android or SQLite directly on either platform, to prove your data-access code produces correct schema and query results, independent of the network. Test the repository-plus-sync-worker layer together, with the network backend simulated rather than real (see below), to prove the sync worker correctly reads pending local changes, calls the network layer, and writes results back to that same real Room or SQLite instance in the right order, including on partial failure. Keep these as two separate integration tests rather than one combined test, since combining them makes a failure harder to localize (is the bug in the local DB layer or the sync logic?) without adding real additional confidence.
Simulating backend behavior
Three mechanisms are available here, and they trade off differently. A hand-written STUB buried in application code (a fake repository or network-client implementation swapped in for the real one) is the fastest to write and fully deterministic, but it never exercises your actual networking code, so it can silently drift from the real API's behavior (serialization bugs, header handling, timeout behavior) with nothing to catch the drift. A MOCK of the HTTP client itself (a mocking framework intercepting calls at the client-library boundary and returning canned responses) is similarly fast and deterministic and shares that same drift risk, plus your real request-building and response-parsing code still never actually runs. A lightweight local TEST SERVER (an in-process HTTP server, or a tool like WireMock/MockServer) closes both gaps: it lets the sync worker make REAL HTTP calls over a real socket, proving the networking code itself works, including serialization and error handling, while still being fully under the test's control for response content and timing, including deliberately simulating a slow response, a partial failure, or a dropped connection mid-sync, scenarios that are difficult to reliably trigger against a real backend on demand. For this integration layer specifically, prefer the test server over either a stub or a client-level mock, since the whole point of this seam is proving the sync worker's real network-handling code, not just its response-handling logic in isolation.
Validating database state and background work
After running a sync cycle against the simulated backend, assert directly against the local database's actual state (querying the real local database the same way the app itself would, not just inspecting the sync worker's return value), since the real risk in sync logic is exactly a case where the worker reports success but leaves the local database in an inconsistent state. For background work specifically, use the platform's real background-task testing utilities (rather than just calling the worker's logic function directly) where feasible, since a background worker's real risk includes platform-level details, being killed and resumed, running with restricted resources, that calling the underlying function directly in a test would not exercise.
Keeping these tests fast and reliable in CI
Run all of this against a real but LOCAL and ephemeral database instance (in-memory or freshly created per test run) rather than a shared test database, so tests don't interfere with each other and don't depend on CI environment state from a previous run. Keep the simulated backend server's responses deterministic and explicitly configured per test case, rather than reused across many tests, so a flaky-looking sync test can always be traced to a specific, known input rather than shared, drifting test-server state.
Trade-offs and pitfalls
The common mistake is testing the sync worker only against ideal-case responses from the simulated backend, when the sync worker's actual job is correctly handling the imperfect cases: partial writes, network timeouts mid-sync, and conflicting concurrent local and remote changes. Since these are exactly the cases a real backend is hardest to reliably trigger on demand, and exactly the cases a purely happy-path simulated backend would never exercise, deliberately including failure-mode test cases against the simulated backend is where most of this integration layer's real value comes from.
Unlock Full Question Bank
Get access to all 6 Test Levels and the Test Pyramid interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.