CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-robolectric--robolectric

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.

Pending
Overview
Eval results
Files

looper-threading.mddocs/

Looper and Threading Control

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.

Capabilities

Looper Mode Configuration

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;

Legacy Scheduler Control (Deprecated)

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);
}

ShadowLooper Control (Recommended)

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();

SystemClock Control

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);

Handler and Message Control

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);
}

Threading Patterns by Mode

PAUSED Mode (Recommended)

@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();
    }
}

INSTRUMENTATION_TEST Mode

@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");
    }
}

Legacy Mode (Not Recommended)

@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();
    }
}

Best Practices

Migration from LEGACY to PAUSED

  1. Replace Scheduler APIs:

    // Old (LEGACY)
    Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();
    
    // New (PAUSED)
    shadowOf(Looper.getMainLooper()).idle();
  2. 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();
  3. Time Management:

    // Old (LEGACY)
    scheduler.advanceBy(5000);
    
    // New (PAUSED)
    ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
    shadowOf(Looper.getMainLooper()).idle();

Test Cleanup

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();
    }
}

Debugging Tips

  1. Check for Unexecuted Tasks:

    ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());
    if (mainLooper.hasQueuedTasks()) {
        System.out.println("Unexecuted tasks: " + mainLooper.size());
    }
  2. Inspect Next Task Time:

    long nextTaskTime = mainLooper.getNextScheduledTaskTime();
    System.out.println("Next task at: " + nextTaskTime);
  3. Use Incremental Execution:

    // Execute one task at a time for debugging
    while (mainLooper.runOneTask()) {
        // Inspect state after each task
    }

Common Issues and Solutions

  1. "Main looper has queued unexecuted runnables": Add shadowOf(getMainLooper()).idle() calls in your test

  2. Background tasks not executing: Ensure you're controlling the correct Looper for background threads

  3. Time-based tests flaky: Use ShadowSystemClock.advanceBy() instead of Thread.sleep()

  4. UI updates not reflecting: Call shadowOf(Looper.getMainLooper()).idle() after UI operations

Install with Tessl CLI

npx tessl i tessl/maven-org-robolectric--robolectric

docs

component-lifecycle.md

index.md

looper-threading.md

runtime-environment.md

shadow-system.md

test-runner.md

tile.json