What is the thread-local scope of a MockedStatic, and how does it affect concurrent or async code under test?
answer
- interceptor is thread-local
- other threads see the real static
- ExecutorService / CompletableFuture / @Async / parallelStream break it
- fix: direct/same-thread executor
- best fix: inject the dependency
basics
~20 sA MockedStatic only intercepts static calls made on the same thread that created it. Code under test that runs the static call on a different thread (a thread pool, an async task) sees the real method, not your stub.
solid answer
~50 sMockito's static mocking is thread-local: the interceptor created by mockStatic applies only to calls executed on the thread that opened the scope. This keeps tests isolated and lets parallel tests mock the same static type independently without stepping on each other. The catch is that any code under test which dispatches the static call onto another thread — an ExecutorService, CompletableFuture.supplyAsync, a @Async method, a reactive scheduler — runs that call on a worker thread where the mock is not installed, so it gets the real static behavior and your stub is silently ignored. To test such code you either run it on the test thread (e.g. a same-thread/direct executor), set up the mock on the worker thread, or, better, refactor to inject the dependency so you don't rely on static interception crossing threads at all.
go deeper
Aware that static mocks behave per-test; may not yet know the boundary is the thread.
Knows the mock is thread-scoped and that async code can bypass it, and can use a synchronous executor to keep work on the test thread.
Explains why thread-local scoping enables parallel tests, identifies the async sources that break it, and chooses between same-thread execution and refactoring.
Pushes injection over static interception precisely so tests don't depend on thread placement, and sets concurrency-testing conventions that avoid the trap.
## What 'thread-local scope' means A **thread** is an independent line of execution; a JVM runs many concurrently. **Thread-local** state is state that each thread sees its own copy of. When Mockito installs a static interceptor via `mockStatic`, it records the active mock in a thread-local registry: a given thread's static calls are intercepted **only if that same thread opened the scope**. ## Why Mockito did this 1. **Test isolation.** Two tests running in parallel (JUnit 5 parallel execution) can each `mockStatic(Clock.class)` with different stubs and not collide, because each test's mock lives on its own thread. 2. **Bounded blast radius.** A leaked mock can only affect the same thread, not arbitrary background threads in the JVM. ## The trap with concurrency / async Mocking is installed on the **test thread**. If the code under test moves the static call to *another* thread, the mock is not there: ```java try (MockedStatic<Instant> mocked = mockStatic(Instant.class)) { mocked.when(Instant::now).thenReturn(fixed); // runs on a pool thread -> Instant.now() is REAL, not `fixed` Instant t = executor.submit(() -> service.timestampedAsync()).get(); } ``` Common sources of a 'wrong thread': `ExecutorService.submit/execute`, `CompletableFuture.supplyAsync` (default ForkJoinPool), Spring `@Async`, `@Scheduled`, reactive schedulers (`subscribeOn`/`publishOn`), parallel streams (`parallelStream()` uses the common pool). On those threads your stub does nothing and the test either fails non-deterministically or passes for the wrong reason. ## How to deal with it - **Make it run on the test thread.** Use a synchronous/direct executor (e.g. Guava `directExecutor()` or `MoreExecutors.newDirectExecutorService()`), or `CompletableFuture.completedFuture(...)` style seams, so the static call executes on the test thread where the mock lives. - **Set up the mock on the worker thread.** Possible but awkward and rarely worth it. - **Refactor away the static.** Inject a `Clock`, `Supplier<Instant>`, or an interface and pass it through to the async task. Now you mock an ordinary object that travels with the code regardless of thread — and you stop fighting thread-locality entirely. This is the recommended fix and ties back to the broader 'mocking statics is a design smell' point. ## Mental model Think of the static mock as a sticky note placed on *one thread's desk*. Work handed to a different desk (thread) never sees the note. If your test needs the note to be seen, either keep the work on the same desk or stop using sticky notes and hand the dependency to the worker directly.
- Your async test sometimes passes and sometimes fails. The static stub looks correct. What's a likely cause?The static call executes on a worker thread where the thread-local mock isn't installed, so it returns the real value; whether the assertion passes then depends on timing/real data, hence the flakiness.
- Name two ways to fix it.Run the work on the test thread via a direct/same-thread executor so the call hits the mocked thread; or refactor to inject a Clock/Supplier so the dependency travels with the task and no static interception is needed.
saying these in an interview costs you the question
- Assuming a static mock is JVM-global and will apply to pool/worker threads.
- Writing an async test that passes because the real static happened to match, not because the stub fired.
- Trying to mock on the test thread to control a CompletableFuture.supplyAsync running on the common ForkJoinPool.