The industry-standard unit testing framework for Android that enables running tests in a simulated Android environment inside a JVM without requiring an emulator or device.
—
Comprehensive control over Android's Looper and threading behavior with multiple modes for different testing scenarios, from legacy scheduler-based control to realistic paused execution and instrumentation test simulation.
Primary annotation for controlling Robolectric's Looper and threading behavior.
/**
* Configurer annotation for controlling Robolectric's Looper behavior.
* Currently defaults to PAUSED behavior, can be overridden at package/class/method level
* or via 'robolectric.looperMode' system property.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
public @interface LooperMode {
/** Set the Looper mode */
Mode value();
/** Supported Looper modes */
enum Mode {
/**
* @deprecated Robolectric's default threading model prior to 4.4
*
* Tasks posted to Loopers are managed via Scheduler.
* Only single Looper thread - tests and posted tasks execute on same thread.
*
* Problems with this mode:
* - Default UNPAUSED state executes tasks inline synchronously (differs from Android)
* - Scheduler list can get out of sync with MessageQueue causing deadlocks
* - Each Scheduler keeps own time value that can desync
* - Background Looper tasks execute in main thread causing thread enforcement errors
*/
@Deprecated
LEGACY,
/**
* Mode that accurately models real Android Looper behavior (default).
*
* Similar to LEGACY PAUSED in these ways:
* - Tests run on main looper thread
* - Main Looper tasks not executed automatically, need explicit ShadowLooper APIs
* - SystemClock time is frozen, manually advanced via Robolectric APIs
*
* Improvements over LEGACY:
* - Warns if test fails with unexecuted tasks in main Looper queue
* - Robolectric test APIs automatically idle main Looper
* - Each Looper has own thread, background loopers execute asynchronously
* - Loopers use real MessageQueue for pending tasks
* - Single clock value managed via ShadowSystemClock
*/
PAUSED,
/**
* Simulates instrumentation test threading model with separate test thread
* distinct from main looper thread.
*
* Similar to PAUSED mode but with separate test and main threads.
* Clock time still fixed, can use shadowLooper methods for control.
* Recommended for tests using androidx.test APIs.
* Most org.robolectric APIs that interact with UI will raise exception
* if called off main thread.
*/
INSTRUMENTATION_TEST
// RUNNING mode planned for future - free running threads with auto-increasing clock
}
}Usage Examples:
// Class-level configuration
@LooperMode(LooperMode.Mode.PAUSED)
public class MyTest {
// All tests use PAUSED mode
}
// Method-level override
@LooperMode(LooperMode.Mode.PAUSED)
public class MyTest {
@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) // Overrides class setting
@Test
public void instrumentationStyleTest() {
// This test runs with separate test thread
}
}
// Package-level configuration in package-info.java
@LooperMode(LooperMode.Mode.PAUSED)
package com.example.tests;Scheduler-based control for LEGACY looper mode. Strongly recommended to migrate to PAUSED mode.
public class Robolectric {
/**
* @deprecated Use PAUSED Looper mode with ShadowLooper APIs instead
* Return foreground scheduler (UI thread scheduler).
*/
@Deprecated
public static Scheduler getForegroundThreadScheduler();
/**
* @deprecated Use ShadowLooper.runToEndOfTasks() instead
* Execute all runnables enqueued on foreground scheduler.
*/
@Deprecated
public static void flushForegroundThreadScheduler();
/**
* @deprecated Use PAUSED Looper mode instead
* Return background scheduler.
*/
@Deprecated
public static Scheduler getBackgroundThreadScheduler();
/**
* @deprecated Use ShadowLooper.runToEndOfTasks() instead
* Execute all runnables enqueued on background scheduler.
*/
@Deprecated
public static void flushBackgroundThreadScheduler();
}public class RuntimeEnvironment {
/**
* @deprecated Use PAUSED Looper mode instead
* Retrieves current master scheduler used by main Looper and optionally all Loopers.
*/
@Deprecated
public static Scheduler getMasterScheduler();
/**
* @deprecated Use PAUSED Looper mode instead
* Sets current master scheduler. Primarily for core setup, changing during test
* will have unpredictable results.
*/
@Deprecated
public static void setMasterScheduler(Scheduler masterScheduler);
}Modern Looper control APIs for PAUSED and INSTRUMENTATION_TEST modes.
/**
* Shadow implementation providing control over Looper behavior.
* Recommended approach for controlling task execution in tests.
*/
public class ShadowLooper {
/** Get ShadowLooper for main Looper */
public static ShadowLooper shadowMainLooper();
/** Get ShadowLooper for specific Looper */
public static ShadowLooper shadowOf(Looper looper);
// Task execution control
/** Execute all currently queued tasks */
public void idle();
/** Execute tasks for specified duration, advancing clock */
public void idleFor(long time, TimeUnit timeUnit);
public void idleFor(Duration duration);
/** Execute only tasks scheduled before current time */
public void runToEndOfTasks();
/** Execute next queued task */
public boolean runOneTask();
/** Execute tasks until next delayed task or queue empty */
public void runToNextTask();
// Queue inspection
/** Check if Looper has queued tasks */
public boolean hasQueuedTasks();
/** Get next task execution time */
public long getNextScheduledTaskTime();
/** Get number of queued tasks */
public int size();
// Clock control
/** Get current Looper time */
public long getCurrentTime();
// Looper state control
/** Pause Looper (tasks won't execute automatically) */
public void pause();
/** Unpause Looper (tasks execute when posted) */
public void unPause();
/** Check if Looper is paused */
public boolean isPaused();
/** Reset Looper state */
public void reset();
}Usage Examples:
import static org.robolectric.Shadows.shadowOf;
// Main looper control
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
// Execute all pending tasks
mainLooper.idle();
// Execute tasks for 5 seconds
mainLooper.idleFor(Duration.ofSeconds(5));
// Check for pending work
if (mainLooper.hasQueuedTasks()) {
mainLooper.runOneTask();
}
// Background looper control
HandlerThread backgroundThread = new HandlerThread("background");
backgroundThread.start();
ShadowLooper backgroundLooper = shadowOf(backgroundThread.getLooper());
backgroundLooper.idle();Clock manipulation for time-sensitive testing in PAUSED and INSTRUMENTATION_TEST modes.
/**
* Shadow for SystemClock providing clock control in tests.
*/
public class ShadowSystemClock {
/** Advance system clock by specified amount */
public static void advanceBy(Duration duration);
public static void advanceBy(long time, TimeUnit timeUnit);
/** Set absolute system time */
public static void setCurrentTimeMillis(long millis);
/** Get current system time */
public static long currentTimeMillis();
/** Sleep current thread for specified time without advancing clock */
public static void sleep(Duration duration);
public static void sleep(long time, TimeUnit timeUnit);
}Usage Example:
// Start with known time
ShadowSystemClock.setCurrentTimeMillis(1000000000L);
// Advance time by 1 hour
ShadowSystemClock.advanceBy(Duration.ofHours(1));
// Verify time-based behavior
assertThat(System.currentTimeMillis()).isEqualTo(1000000000L + 3600000L);Fine-grained control over Handler and Message execution.
/**
* Shadow for Handler providing message control.
*/
public class ShadowHandler {
/** Get ShadowHandler for specific Handler */
public static ShadowHandler shadowOf(Handler handler);
// Message queue inspection
/** Check if Handler has pending messages */
public boolean hasMessages(int what);
public boolean hasMessages(int what, Object object);
/** Get queued messages */
public List<Message> getMessages();
// Message execution control
/** Execute all queued messages */
public void flush();
/** Execute specific message */
public void handleMessage(Message message);
}@LooperMode(LooperMode.Mode.PAUSED)
public class PausedModeTest {
@Test
public void testAsyncOperation() {
// Start async operation
myService.performAsyncOperation();
// Tasks are queued but not executed automatically
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
assertThat(mainLooper.hasQueuedTasks()).isTrue();
// Explicitly execute tasks
mainLooper.idle();
// Verify result
assertThat(myService.isOperationComplete()).isTrue();
}
@Test
public void testDelayedOperation() {
// Post delayed task (5 seconds)
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(() -> myCallback.onComplete(), 5000);
// Advance time and execute
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
mainLooper.idleFor(Duration.ofSeconds(5));
// Verify callback executed
verify(myCallback).onComplete();
}
}@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)
public class InstrumentationTest {
@Test
public void testWithSeparateTestThread() {
// Test runs on separate thread from main Looper
// Can use androidx.test APIs that expect this pattern
// Use CountDownLatch for synchronization
CountDownLatch latch = new CountDownLatch(1);
// Post to main thread
new Handler(Looper.getMainLooper()).post(() -> {
// UI operations on main thread
myActivity.updateUI();
latch.countDown();
});
// Wait for completion
latch.await(5, TimeUnit.SECONDS);
// Verify result
assertThat(myActivity.getDisplayText()).isEqualTo("Updated");
}
}@SuppressWarnings("deprecation")
@LooperMode(LooperMode.Mode.LEGACY)
public class LegacyModeTest {
@Test
public void testWithScheduler() {
// Use deprecated scheduler APIs
Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
// Control execution
scheduler.pause();
myService.performAsyncOperation();
// Manually advance
scheduler.advanceToLastPostedRunnable();
// Verify result
assertThat(myService.isOperationComplete()).isTrue();
}
}Replace Scheduler APIs:
// Old (LEGACY)
Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
// New (PAUSED)
shadowOf(Looper.getMainLooper()).idle();Handle Background Threads:
// Old (LEGACY) - everything on main thread
Robolectric.getBackgroundThreadScheduler().advanceToLastPostedRunnable();
// New (PAUSED) - real background threads
HandlerThread thread = new HandlerThread("background");
thread.start();
shadowOf(thread.getLooper()).idle();Time Management:
// Old (LEGACY)
scheduler.advanceBy(5000);
// New (PAUSED)
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
shadowOf(Looper.getMainLooper()).idle();Always ensure proper cleanup to avoid test pollution:
@After
public void cleanup() {
// Reset looper state
shadowOf(Looper.getMainLooper()).reset();
// Clean up background threads
if (backgroundThread != null) {
backgroundThread.quitSafely();
}
}Check for Unexecuted Tasks:
ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
if (mainLooper.hasQueuedTasks()) {
System.out.println("Unexecuted tasks: " + mainLooper.size());
}Inspect Next Task Time:
long nextTaskTime = mainLooper.getNextScheduledTaskTime();
System.out.println("Next task at: " + nextTaskTime);Use Incremental Execution:
// Execute one task at a time for debugging
while (mainLooper.runOneTask()) {
// Inspect state after each task
}"Main looper has queued unexecuted runnables": Add shadowOf(getMainLooper()).idle() calls in your test
Background tasks not executing: Ensure you're controlling the correct Looper for background threads
Time-based tests flaky: Use ShadowSystemClock.advanceBy() instead of Thread.sleep()
UI updates not reflecting: Call shadowOf(Looper.getMainLooper()).idle() after UI operations
Install with Tessl CLI
npx tessl i tessl/maven-org-robolectric--robolectric