Free Testing Interview Questions & Answers

Unit, coroutine, Flow, and Compose testing with the right test doubles.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What is the practical difference between a local unit test and an instrumented test, and where does each run?

    Local unit tests live under src/test and run on your development machine's JVM with no Android framework, so they are fast but any real Android class returns a stubbed method that throws by default. Instrumented tests live under src/androidTest and run on a device or emulator inside an Android runtime, so they can touch real framework classes but are far slower to start. The trap is calling Android APIs like Log or Uri from a local test and getting the notorious method not mocked error, which means you either need Robolectric, a fake, or to move the test to androidTest.

  2. Your local unit test fails with 'Method ... not mocked'. What is actually happening and how do you fix it correctly?

    The android.jar on the unit test classpath is a stub jar where every method throws a RuntimeException unless configured otherwise, so calling something like TextUtils.isEmpty or Log.d explodes because there is no Android runtime. The lazy fix of setting testOptions unitTests.returnDefaultValues true makes those methods return zero, false, or null, which often hides bugs rather than testing them. The correct approach is to not depend on framework classes in pure logic, or to run the test with Robolectric which provides real shadow implementations, or to promote it to an instrumented test when it genuinely needs the framework.

  3. Name the five classic test doubles and give a one-line distinction between each.

    A dummy is a placeholder passed only to satisfy a parameter and never actually used, a stub returns canned answers to calls made during the test, a fake is a working lightweight implementation such as an in-memory database that behaves like the real thing, a mock is preprogrammed with expectations and verifies that specific interactions occurred, and a spy wraps a real object recording how it was called while optionally overriding parts. The subtle point is that mock and stub differ in intent: a stub supports state verification while a mock supports behavior verification, and conflating them leads to tests that assert on the wrong thing.

  4. Why do fakes usually beat mocks when testing a repository?

    A mock forces you to encode the repository's internal call sequence into the test, so the test knows too much about implementation and breaks whenever you refactor even though behavior is unchanged. A fake repository backed by an in-memory map lets you exercise real save-then-read semantics, ordering, and edge cases through the public contract, so tests assert on observable state rather than on interaction choreography. This makes fakes more robust and more honest, and the fake can be reused across many test classes, whereas a wall of every-when-then mock setup tends to be copy-pasted and brittle.

  5. In MockK, what does a relaxed mock do and when is it a trap?

    A relaxed mock automatically returns sensible defaults for every function without you stubbing each one, returning zero, empty string, or an empty mock for reference types, which reduces boilerplate when you only care about a couple of calls. The trap is that it silently swallows unstubbed calls, so a method you forgot about returns a fake empty object and the test passes for the wrong reason, masking a real integration gap. Prefer a strict mock or use relaxUnitFun true to relax only Unit-returning functions, so meaningful return values still force you to declare intent.

  6. How do you stub a suspend function in MockK, and what breaks if you use the wrong builder?

    You stub a suspend function with coEvery and verify it with coVerify, because these open a suspending block where you can call other suspend functions, whereas the plain every and verify builders are non-suspending and will not compile against a suspend member. Using every on a suspend function is a common early mistake that produces confusing compile errors or forces awkward runBlocking wrappers. The same coEvery machinery lets you return values, throw, or use coAnswers to run custom suspending logic during the stubbed call.

  7. What is mockkStatic for, and why is it needed even for Kotlin top-level functions?

    mockkStatic intercepts static or top-level functions and Java static methods so you can stub calls like Uri.parse or a Kotlin file-level function that compiles down to a static method on a synthetic FileKt class. It is needed because MockK can only redirect a normal instance call through a proxy, but static and top-level functions have no instance, so MockK must instrument the class instead. You should pair it with unmockkStatic or unmockkAll in teardown, otherwise the instrumentation leaks across tests and causes bizarre cross-contamination failures.

  8. What is the key difference between MockK and Mockito for Kotlin projects?

    MockK is written for Kotlin and natively understands suspend functions, coroutines, final classes, objects, extension functions, and top-level functions, which matters because Kotlin classes are final by default. Mockito historically could not mock final classes without the mock-maker-inline agent, and it needs mockito-kotlin plus extra plumbing to handle suspend functions and to avoid null-return crashes on non-null Kotlin types. The practical upshot is that MockK feels idiomatic in Kotlin, while Mockito is battle-tested and lighter but keeps fighting Kotlin's finality and null-safety.

  9. How does JUnit4 differ from JUnit5 in ways that affect an Android test suite?

    JUnit4 uses the Runner and Rule model with annotations like Before and RunWith, while JUnit5 splits into the Jupiter API with BeforeEach, extensions instead of rules, nested tests, and parameterized tests built in. The Android trap is that the instrumented test runner and Espresso are built around JUnit4, and AndroidJUnitRunner does not run Jupiter tests, so on-device tests generally stay on JUnit4 while JUnit5 is used mainly for local JVM tests. Mixing them requires the vintage engine or careful configuration, so many Android codebases pragmatically standardize on JUnit4.

  10. What does runTest give you over runBlocking when testing coroutines?

    runTest installs a virtual-time scheduler so delay calls are skipped rather than actually waited on, which makes tests that model timeouts or debouncing run instantly instead of in real seconds. It also aggregates uncaught child-coroutine failures and fails the test, auto-advances time when the test body suspends, and enforces that all launched coroutines complete or it reports them. runBlocking by contrast really blocks the thread and really sleeps on delay, so it is slower and does not manage virtual time or surface leaked children, making it the wrong tool for structured coroutine testing.

  11. What is the difference between StandardTestDispatcher and UnconfinedTestDispatcher?

    StandardTestDispatcher queues new coroutines rather than running them immediately, so nothing launched inside your code executes until you call advanceUntilIdle, runCurrent, or advanceTimeBy, giving you precise control over interleaving. UnconfinedTestDispatcher instead runs coroutines eagerly and depth-first up to their first real suspension point, so launched work starts immediately, which is convenient for simple emit-then-assert tests. The gotcha is that eager execution can hide ordering bugs and does not represent how a real dispatcher schedules, so choose Standard when order matters and Unconfined only when you deliberately want immediate execution.

  12. What do advanceUntilIdle and advanceTimeBy do, and how do they differ?

    advanceUntilIdle runs everything the test scheduler has queued, including all pending delays, until there is no more work left, which is the blunt instrument for driving a coroutine to completion. advanceTimeBy moves virtual time forward by a specific duration, executing only the tasks scheduled to fire within that window, which lets you assert intermediate states such as what happens after 500 milliseconds of a debounce but before it fires. The distinction matters when you are testing timing behavior precisely, because advanceUntilIdle would blow past the intermediate moment you wanted to observe.

  13. Why should you inject a dispatcher rather than reference Dispatchers.IO directly, and how does it help tests?

    Hardcoding Dispatchers.IO or Dispatchers.Main inside a class makes it impossible to substitute a test dispatcher, so your test runs on real threads with real scheduling and cannot use virtual time, producing slow or flaky results. Injecting a dispatcher, often via a small DispatcherProvider interface, lets tests pass a TestDispatcher that shares the scheduler with runTest so all timing is controlled and deterministic. This is the coroutine equivalent of dependency injection for the clock, and it is the single most common fix for coroutine tests that hang or behave nondeterministically.

  14. What is the MainDispatcherRule pattern and what problem does it solve?

    Dispatchers.Main is backed by the Android main looper, which does not exist in a local JVM unit test, so any viewModelScope launch fails with a missing Main dispatcher error. The MainDispatcherRule is a small JUnit rule that calls Dispatchers.setMain with a TestDispatcher before each test and Dispatchers.resetMain afterward, allowing Main-bound code to run under the test scheduler. Sharing that dispatcher's scheduler with your runTest call ties everything to one virtual clock, which is what lets ViewModel tests advance time and observe emissions deterministically.

  15. Why can collecting a hot StateFlow in a test hang, and how do you avoid it?

    A StateFlow never completes, so writing a for-loop or toList over it blocks forever waiting for a terminal event that never comes, and even launching a collector inside runTest can leave the test waiting on that never-ending coroutine. The fix is to collect in a separately launched coroutine you cancel yourself, or better to use Turbine which manages a collection scope and lets you pull emissions one at a time and then cancel. If you only need the current value you can just read stateFlow.value, but if you need to assert on the sequence of emissions you must bound the collection explicitly.

  16. How does Turbine's awaitItem work, and what is cancelAndConsumeRemainingEvents for?

    Inside a test block, awaitItem suspends until the flow emits the next value and returns it, so you assert emissions in order without manual collector plumbing, and Turbine fails the test if the flow errors or completes when you expected another item. cancelAndConsumeRemainingEvents stops collecting and drains any buffered events so the test does not fail for unconsumed emissions, which is essential for hot flows like StateFlow that would otherwise never complete. The discipline Turbine enforces is that every emission must be accounted for, which catches both missing and extra emissions that a naive test would miss.

  17. When testing a StateFlow, when should you assert on value versus on emissions?

    Reading value gives you the current conflated state and is perfect for asserting the final settled state after you advance the dispatcher, but it cannot tell you whether an intermediate loading state was ever emitted because conflation may have dropped it. If the contract is that the UI must pass through a Loading state before Success, you must collect emissions with Turbine and assert the ordered sequence, since checking only the final value would let a broken implementation that skips Loading pass. So value tests final state and Turbine tests the transition sequence.

  18. Why is StateFlow conflation a trap in emission-based tests?

    StateFlow conflates by keeping only the latest value and skips intermediate values if the collector is slower than the producer, so if your code sets state to Loading and then immediately to Success within the same dispatch, a collector that starts late may only ever see Success. Under a test dispatcher this timing can shift depending on when you start collecting relative to when you advance time, causing a test to see one emission on one run and two on another. To reliably observe intermediate states you must start the Turbine collection before triggering the action and control advancement so each state is actually observed.

  19. What does createComposeRule give you and how does it drive the UI clock?

    createComposeRule provides a ComposeTestRule that hosts your composable under test, exposes finders like onNodeWithText and onNodeWithTag, and controls a virtual test clock through mainClock so recompositions and animations are deterministic. By default it also synchronizes with Compose's idling so assertions wait until the UI is stable rather than racing the frame. There is also createAndroidComposeRule when you need a real Activity context, but the plain createComposeRule is lighter and preferred when you do not need Android-specific integration.

  20. What is the difference between onNodeWithText and onNodeWithTag, and why prefer testTag?

    onNodeWithText matches a node by its displayed or content text, which couples the test to user-visible copy that changes with localization or wording tweaks, while onNodeWithTag matches a stable identifier you attach with Modifier.testTag. Preferring testTag makes tests resilient to copy changes and unambiguous when several nodes share text, and it does not affect production behavior because tags live in the semantics tree. The nuance is that you should still assert on real user-visible content somewhere, so use tags to locate nodes and text assertions to verify what the user actually sees.

  21. What is the semantics tree in Compose testing and why does it matter?

    Compose does not expose a view hierarchy, so the test framework and accessibility services both read a parallel semantics tree that each composable populates with properties like text, role, click actions, and state descriptions. Your matchers query this tree, which means a composable with no meaningful semantics is effectively invisible to tests and to screen readers alike, so writing testable UI and writing accessible UI reinforce each other. When a node cannot be found, the usual cause is missing or merged semantics, and printToLog on the tree is the standard debugging step.

  22. When do you need waitForIdle or mainClock.autoAdvance in a Compose test?

    The rule normally auto-synchronizes, advancing the clock and waiting until Compose is idle before your assertion runs, so simple tests need nothing special. For animations or infinite transitions you may set mainClock.autoAdvance to false to freeze the clock and then advance it frame by frame with advanceTimeBy, which lets you assert mid-animation states deterministically instead of racing real time. waitForIdle explicitly blocks until pending work and recompositions settle, which you reach for when you have manually disabled auto-advance or are coordinating with non-Compose async work.

  23. Why can a Compose test hang forever, and how does autoAdvance relate to it?

    The test framework waits for the app to become idle before proceeding, so an indefinitely running animation, an infinite rememberInfiniteTransition, or a coroutine that never completes keeps the clock busy and the test blocks waiting for idleness that never arrives. The fix is to set mainClock.autoAdvance to false and manually advance the clock so the never-ending animation cannot stall synchronization, then assert the frames you care about. This is the Compose analog of the hot-flow hang: an unbounded source of work confuses the idle-based synchronization.

  24. What is an IdlingResource in Espresso and why is it needed?

    Espresso automatically waits for the main thread message queue and AsyncTask pools to be idle, but it has no visibility into your own background work such as a custom thread pool, an OkHttp dispatcher, or a coroutine on Dispatchers.IO, so it may assert before the async result arrives. An IdlingResource is an object Espresso polls that reports whether your async operation is still in flight, letting Espresso wait until it becomes idle before continuing. The correct pattern registers and unregisters it around the test and drives isIdleNow from your actual in-flight counter, rather than sprinkling Thread.sleep which is the classic flaky anti-pattern.

  25. Why is Thread.sleep a bad way to wait for async work in a UI test?

    A fixed sleep either wastes time when the work finishes early or fails intermittently when the device is slow and the work has not finished, so it trades one flake for another and slows the whole suite. It encodes a guess about timing rather than a fact about completion, and CI machines under load routinely blow past the guessed interval. The correct alternatives are IdlingResource for Espresso, the Compose idling synchronization, or Turbine and virtual time for coroutines, all of which wait for the actual condition rather than a clock.

  26. What are the tradeoffs between Robolectric and on-device instrumented tests?

    Robolectric runs Android code on the JVM using shadow implementations of framework classes, so tests start in seconds without an emulator and run cheaply in CI, but the shadows are approximations that can diverge from real device behavior and lag new API levels. On-device or emulator instrumentation exercises the genuine framework, real rendering, and real hardware quirks, giving higher fidelity at the cost of slow startup and flakier infrastructure. The pragmatic split is Robolectric for fast feedback on framework-touching logic and a smaller set of instrumented tests for true integration and rendering confidence.

  27. How do you test a Room DAO correctly, and why an in-memory database?

    You build the database with Room.inMemoryDatabaseBuilder so the data lives only in RAM and is discarded when the process ends, which keeps each test isolated and fast without touching disk or leaking state between runs. You typically allow main-thread queries only in the test for convenience and close the database in teardown to release resources. The important nuance is that an in-memory Room database is still the real SQLite engine and real generated DAO code, so it validates your actual queries and type converters, unlike a mock DAO which would only re-assert your own assumptions.

  28. Why is mocking a Room DAO usually a bad idea?

    The value of a DAO test is confidence that your SQL, indices, relations, and type converters behave correctly, and a mocked DAO returns whatever you tell it, so it proves nothing about the queries themselves and just restates your assumptions. Real bugs live in the SQL and in Room's generated code, which a mock bypasses entirely, so the test passes while the query is broken. Using an in-memory database instead exercises the genuine engine, catching malformed queries, missing migrations, and converter mistakes that a mock can never reveal.

  29. How do you test a Room migration, and what does the tooling provide?

    Room ships a MigrationTestHelper that lets you create a database at an old schema version using exported schema JSON, insert data, run the migration, and then validate the resulting schema and that data survived. This requires enabling schema export in the Room compiler options so the JSON files exist for each version, which the helper reads to reconstruct the old database. The subtle point is that auto-generated validation only checks structure, so you must also insert representative rows and assert them after migrating to prove that a column rename or data transformation preserved user data rather than dropping it.

  30. How do you make time testable, and why is injecting a Clock better than fakes for now()?

    Code that calls System.currentTimeMillis or Instant.now directly is nondeterministic because now changes every run, so you inject a Clock abstraction and in tests supply a fixed clock that always returns a known instant, making expiry, scheduling, and formatting deterministic. Java's java.time.Clock is designed exactly for this with Clock.fixed, and passing it into constructors keeps the seam explicit. This turns flaky time-dependent assertions into stable ones, and it lets you fast-forward logical time in tests by swapping the clock rather than sleeping.

  31. What does @HiltAndroidTest do and what is HiltAndroidRule responsible for?

    @HiltAndroidTest generates a Hilt test component for the test class so dependencies can be injected into the test and into the components under test, replacing the normal application-level Hilt graph with a test graph. HiltAndroidRule performs the injection when its inject method is called and manages the component lifecycle around each test, and it must run before other rules that depend on injected fields, which is why rule ordering with order parameters matters. You also need a Hilt test runner, typically a custom AndroidJUnitRunner that swaps in HiltTestApplication, otherwise the test application is not Hilt-enabled.

  32. How do @UninstallModules and @TestInstallIn let you swap Hilt bindings in tests?

    @UninstallModules removes a production module from the test's component so its bindings are gone, letting you provide test replacements in a module declared inside the test class for a narrow, per-test substitution. @TestInstallIn is the broader mechanism that replaces a production module across the whole test source set by naming the module it supersedes, which is cleaner when every test should use the same fake. The gotcha is that uninstalling a module also removes every binding it provided, so you must re-provide all of them, and mixing the two approaches carelessly leads to duplicate-binding compile errors.

  33. What is the difference between Paparazzi and instrumented screenshot testing?

    Paparazzi renders Compose or View layouts entirely on the JVM using LayoutLib, the same rendering engine as the Studio preview, so it produces screenshots without an emulator and runs fast in CI, but it approximates the device and cannot capture real GPU rendering, hardware fonts, or actual runtime behavior. Instrumented screenshot tools capture pixels from a real device or emulator, giving true fidelity including system rendering, at the cost of slow, infrastructure-heavy runs sensitive to device font and density differences. The tradeoff mirrors Robolectric versus on-device: Paparazzi for fast deterministic diffs, instrumented for genuine rendering confidence.

  34. Why are screenshot tests prone to flakiness across machines, and how do you stabilize them?

    Screenshot comparisons fail on tiny pixel differences caused by font rendering, antialiasing, GPU drivers, device density, locale, and system animation state, so a golden image captured on one machine may not match bytes on another. Stabilizing means pinning a fixed device configuration and font, disabling animations, freezing any time or random data shown in the UI, and often allowing a small pixel-difference tolerance. JVM-based tools like Paparazzi reduce this by rendering identically everywhere, whereas on-device screenshots demand a locked emulator image to stay reproducible.

  35. What are the most common root causes of flaky tests, and how do you categorize them?

    The recurring causes are shared mutable state that leaks between tests, reliance on real wall-clock time or delays, real animations, real network calls, nondeterministic ordering from concurrency, and dependence on device configuration or locale. They cluster into three buckets: shared state that should be reset in setup or teardown, timing that should be virtualized with a test dispatcher or fixed clock, and external dependencies that should be replaced with fakes or idling synchronization. Diagnosing flakiness is really a hunt for hidden nondeterminism, and the fix is almost always to make the source of variance injectable and controlled.

  36. How does leftover shared state cause order-dependent test failures, and how do you prevent it?

    A static field, a singleton, an object holding cached data, or a MockK static mock left installed carries state from one test into the next, so a test that passes alone fails when run after another, and the failure moves around when the runner reorders tests. Prevention means resetting or recreating everything per test in a Before or After block, using unmockkAll to tear down MockK, avoiding singletons in code under test or resetting them, and never asserting on a global that another test mutates. A good smoke test is to run the suite in random order, which surfaces these hidden couplings quickly.

  37. What does the given-when-then structure buy you, and how should tests be named?

    Given-when-then, also called arrange-act-assert, separates setup from the single action under test and from the assertions, which keeps each test focused on one behavior and makes failures easy to localize because the act step is obvious. A descriptive name that states the scenario and expected outcome, such as returnsCachedValue_whenNetworkFails, doubles as documentation so a failing test tells you what contract broke without reading the body. The anti-pattern is a test that acts several times and asserts throughout, which tests many things at once and gives a vague signal when it fails.

  38. Why is testing only the happy path insufficient, and what error paths deserve tests?

    Most production incidents come from the unhappy paths, so a suite that only asserts success gives false confidence while leaving exception handling, empty results, timeouts, cancellation, and malformed input untested where the real bugs hide. You should test that a repository maps a network failure to the right error state, that a coroutine respects cancellation, that a timeout produces a recoverable result, and that invalid input is rejected rather than silently corrupting state. Error-path tests are also where naive code and correct code diverge most, since a swallowed exception looks fine until you assert the observable failure behavior.

  39. How do you test that a suspend function propagates cancellation correctly?

    You launch the function in a child coroutine within runTest, cancel that job, and assert that it stopped promptly and cleaned up, and you verify that the function does not swallow the CancellationException, because catching a generic Throwable and continuing breaks structured concurrency. A correct implementation rethrows CancellationException or uses cooperative suspension points so cancellation actually takes effect, while a naive try-catch around the whole body silently ignores it. The tell-tale test is that after cancellation any downstream side effect must not have run, which distinguishes code that merely compiles from code that respects cancellation.

  40. Why is over-mocking brittle, and what is the symptom that you have gone too far?

    Over-mocking replaces so many collaborators that the test asserts on how the code calls its dependencies rather than what it produces, so any refactor that changes the call sequence without changing behavior breaks a pile of tests. The symptom is a test that is mostly every-when-then and verify setup with almost no assertion on real output, and that must be rewritten every time you touch the implementation. The remedy is to mock only true boundaries like the network and use real objects or fakes for everything you own, so the test survives refactoring and actually exercises logic instead of restating it.

  41. Why is high code coverage a misleading measure of test quality?

    Coverage only records that a line executed during some test, not that any assertion checked its result, so you can reach ninety percent coverage with tests that call methods and assert nothing, catching zero regressions. It also cannot see missing cases, since a branch you never wrote a test for simply is not counted, and it rewards testing trivial getters while a subtle concurrency bug in a covered line goes unasserted. Coverage is useful as a rough map of untested regions, but treating it as a target invites gaming, and mutation testing is a far better signal because it checks whether tests actually detect introduced faults.

  42. What is mutation testing and why is it a better quality signal than coverage?

    Mutation testing deliberately introduces small faults into your code, such as flipping a conditional or replacing a return value, then runs the suite and checks whether any test fails, so a surviving mutant means your tests executed that code but did not actually assert on its behavior. This directly measures the thing coverage cannot: whether tests would catch a real bug, exposing assertion-free tests that pad coverage numbers. The cost is that mutation runs are slow because they re-run the suite many times, so teams typically apply it to critical modules rather than the whole codebase.

  43. How do you test a Flow operator chain that includes debounce or a timed delay?

    You run the test with runTest so its virtual scheduler skips real delay, drive the upstream by emitting values at controlled virtual times, and use advanceTimeBy to move just past the debounce window to assert that only the last value survives, or just short of it to assert nothing emitted yet. Collecting the flow with Turbine lets you assert the exact emitted sequence, and because time is virtual the whole test finishes instantly rather than actually waiting for the debounce interval. The key is sharing the test dispatcher's scheduler between the code and runTest so the debounce timer runs on the same virtual clock you are advancing.

  44. When testing a ViewModel that exposes a StateFlow via stateIn with WhileSubscribed, what subtlety bites you?

    stateIn with SharingStarted.WhileSubscribed only runs its upstream while there is an active collector, so if your test reads value without ever collecting, the upstream may never start and you observe only the initial value, making the test wrongly conclude nothing happened. You must start a real collection, for instance with Turbine or a launched collector, so the sharing coroutine activates the upstream, and then advance the dispatcher. This is a frequent source of a passing-but-wrong test, because the naive value read looks reasonable yet the flow was cold and idle the whole time.

  45. What is the difference between verify(exactly = n) intent and asserting on state, and when does each mislead?

    Verifying that a collaborator was called exactly n times checks interaction, which is appropriate for genuine side effects like sending analytics or writing to a network, but using it for data operations couples the test to implementation so a harmless refactor that batches two calls into one breaks it. Asserting on resulting state checks behavior and survives refactoring, but it cannot detect a missing side effect that produces no observable state, such as a fire-and-forget log. The rule of thumb is to verify interactions only at true side-effect boundaries and assert on state everywhere you can observe an outcome.

  46. Why can UnconfinedTestDispatcher make a test pass that should fail, and what is the deeper risk?

    Because UnconfinedTestDispatcher runs launched coroutines eagerly and depth-first, a state transition that in production happens asynchronously appears to complete synchronously in the test, so ordering bugs and missing yields are hidden and the test passes even though the real dispatcher would interleave differently. The deeper risk is a false sense of correctness: the code depends on eager execution that a real Main or IO dispatcher does not provide, so it works in tests and misbehaves in production. When ordering and concurrency correctness matter, StandardTestDispatcher with explicit advancement models real scheduling far more faithfully.

  47. How do you correctly assert that an exception is thrown by a suspend function, and what is the common mistake?

    Inside runTest you wrap the call in assertFailsWith or JUnit's assertThrows around a block that invokes the suspend function, and because those blocks run synchronously within the test coroutine the exception surfaces where you assert it. The common mistake is throwing from inside a separate launch, where the exception propagates to the coroutine's parent and fails the whole runTest as an uncaught exception rather than being caught by your assertion, so you must either call directly, use async and await, or configure a handler. Testing the message or type precisely also matters, since a broad catch that rethrows a generic Exception would let a wrong error type slip through.

  48. Why might a test that uses real Dispatchers.IO inside runTest still pass locally but flake in CI?

    If code launches on the real Dispatchers.IO rather than the injected test dispatcher, that work escapes the test scheduler's virtual time and runs on genuine background threads, so runTest may finish and assert before the background result arrives, and whether it arrives in time depends on machine load. Locally on a fast idle machine the timing happens to work, while a loaded CI runner shifts the race and the assertion fires too early, producing intermittent failures. The fix is to inject the dispatcher everywhere so all coroutines share the test scheduler, which removes the real-thread race entirely and makes execution deterministic.

  49. What are the risks of relaxed mocks combined with suspend functions returning Flow, and how do you avoid a false pass?

    A relaxed MockK mock of a repository whose function returns a Flow will hand back an empty flow by default, so a collector under test simply sees no emissions and your assertions on downstream state may vacuously pass because nothing ever contradicted them. This hides the fact that you forgot to stub the real data, and the test proves only that empty input yields the initial state. Avoid it by using strict mocks or explicitly stubbing coEvery to return flowOf with representative values, so the test drives real data through the pipeline instead of silently testing the empty case.

  50. You need to test a WorkManager worker's logic in isolation; what is the correct approach and the trap?

    For pure logic you extract the work into a testable function or class and unit-test that directly, but to test the worker within its lifecycle you use the WorkManager testing artifacts, either TestListenableWorkerBuilder to build and run a single worker synchronously, or WorkManagerTestInitHelper with a SynchronousExecutor and test driver to control constraints and delays deterministically. The trap is enqueuing the worker against the real WorkManager and asserting on timing, which is nondeterministic and framework-heavy, so tests hang or flake; the test driver instead lets you set constraints met and initial delay met to fire the worker on demand. This keeps the test fast and deterministic while still exercising the real worker contract rather than a mock.

Practice all Testing questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics