or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

component-lifecycle.mdindex.mdlooper-threading.mdruntime-environment.mdshadow-system.mdtest-runner.md

looper-threading.mddocs/

0

# Looper and Threading Control

1

2

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.

3

4

## Capabilities

5

6

### Looper Mode Configuration

7

8

Primary annotation for controlling Robolectric's Looper and threading behavior.

9

10

```java { .api }

11

/**

12

* Configurer annotation for controlling Robolectric's Looper behavior.

13

* Currently defaults to PAUSED behavior, can be overridden at package/class/method level

14

* or via 'robolectric.looperMode' system property.

15

*/

16

@Documented

17

@Retention(RetentionPolicy.RUNTIME)

18

@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})

19

public @interface LooperMode {

20

/** Set the Looper mode */

21

Mode value();

22

23

/** Supported Looper modes */

24

enum Mode {

25

/**

26

* @deprecated Robolectric's default threading model prior to 4.4

27

*

28

* Tasks posted to Loopers are managed via Scheduler.

29

* Only single Looper thread - tests and posted tasks execute on same thread.

30

*

31

* Problems with this mode:

32

* - Default UNPAUSED state executes tasks inline synchronously (differs from Android)

33

* - Scheduler list can get out of sync with MessageQueue causing deadlocks

34

* - Each Scheduler keeps own time value that can desync

35

* - Background Looper tasks execute in main thread causing thread enforcement errors

36

*/

37

@Deprecated

38

LEGACY,

39

40

/**

41

* Mode that accurately models real Android Looper behavior (default).

42

*

43

* Similar to LEGACY PAUSED in these ways:

44

* - Tests run on main looper thread

45

* - Main Looper tasks not executed automatically, need explicit ShadowLooper APIs

46

* - SystemClock time is frozen, manually advanced via Robolectric APIs

47

*

48

* Improvements over LEGACY:

49

* - Warns if test fails with unexecuted tasks in main Looper queue

50

* - Robolectric test APIs automatically idle main Looper

51

* - Each Looper has own thread, background loopers execute asynchronously

52

* - Loopers use real MessageQueue for pending tasks

53

* - Single clock value managed via ShadowSystemClock

54

*/

55

PAUSED,

56

57

/**

58

* Simulates instrumentation test threading model with separate test thread

59

* distinct from main looper thread.

60

*

61

* Similar to PAUSED mode but with separate test and main threads.

62

* Clock time still fixed, can use shadowLooper methods for control.

63

* Recommended for tests using androidx.test APIs.

64

* Most org.robolectric APIs that interact with UI will raise exception

65

* if called off main thread.

66

*/

67

INSTRUMENTATION_TEST

68

69

// RUNNING mode planned for future - free running threads with auto-increasing clock

70

}

71

}

72

```

73

74

**Usage Examples:**

75

76

```java

77

// Class-level configuration

78

@LooperMode(LooperMode.Mode.PAUSED)

79

public class MyTest {

80

// All tests use PAUSED mode

81

}

82

83

// Method-level override

84

@LooperMode(LooperMode.Mode.PAUSED)

85

public class MyTest {

86

@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST) // Overrides class setting

87

@Test

88

public void instrumentationStyleTest() {

89

// This test runs with separate test thread

90

}

91

}

92

93

// Package-level configuration in package-info.java

94

@LooperMode(LooperMode.Mode.PAUSED)

95

package com.example.tests;

96

```

97

98

### Legacy Scheduler Control (Deprecated)

99

100

Scheduler-based control for LEGACY looper mode. Strongly recommended to migrate to PAUSED mode.

101

102

```java { .api }

103

public class Robolectric {

104

/**

105

* @deprecated Use PAUSED Looper mode with ShadowLooper APIs instead

106

* Return foreground scheduler (UI thread scheduler).

107

*/

108

@Deprecated

109

public static Scheduler getForegroundThreadScheduler();

110

111

/**

112

* @deprecated Use ShadowLooper.runToEndOfTasks() instead

113

* Execute all runnables enqueued on foreground scheduler.

114

*/

115

@Deprecated

116

public static void flushForegroundThreadScheduler();

117

118

/**

119

* @deprecated Use PAUSED Looper mode instead

120

* Return background scheduler.

121

*/

122

@Deprecated

123

public static Scheduler getBackgroundThreadScheduler();

124

125

/**

126

* @deprecated Use ShadowLooper.runToEndOfTasks() instead

127

* Execute all runnables enqueued on background scheduler.

128

*/

129

@Deprecated

130

public static void flushBackgroundThreadScheduler();

131

}

132

```

133

134

```java { .api }

135

public class RuntimeEnvironment {

136

/**

137

* @deprecated Use PAUSED Looper mode instead

138

* Retrieves current master scheduler used by main Looper and optionally all Loopers.

139

*/

140

@Deprecated

141

public static Scheduler getMasterScheduler();

142

143

/**

144

* @deprecated Use PAUSED Looper mode instead

145

* Sets current master scheduler. Primarily for core setup, changing during test

146

* will have unpredictable results.

147

*/

148

@Deprecated

149

public static void setMasterScheduler(Scheduler masterScheduler);

150

}

151

```

152

153

### ShadowLooper Control (Recommended)

154

155

Modern Looper control APIs for PAUSED and INSTRUMENTATION_TEST modes.

156

157

```java { .api }

158

/**

159

* Shadow implementation providing control over Looper behavior.

160

* Recommended approach for controlling task execution in tests.

161

*/

162

public class ShadowLooper {

163

/** Get ShadowLooper for main Looper */

164

public static ShadowLooper shadowMainLooper();

165

166

/** Get ShadowLooper for specific Looper */

167

public static ShadowLooper shadowOf(Looper looper);

168

169

// Task execution control

170

171

/** Execute all currently queued tasks */

172

public void idle();

173

174

/** Execute tasks for specified duration, advancing clock */

175

public void idleFor(long time, TimeUnit timeUnit);

176

public void idleFor(Duration duration);

177

178

/** Execute only tasks scheduled before current time */

179

public void runToEndOfTasks();

180

181

/** Execute next queued task */

182

public boolean runOneTask();

183

184

/** Execute tasks until next delayed task or queue empty */

185

public void runToNextTask();

186

187

// Queue inspection

188

189

/** Check if Looper has queued tasks */

190

public boolean hasQueuedTasks();

191

192

/** Get next task execution time */

193

public long getNextScheduledTaskTime();

194

195

/** Get number of queued tasks */

196

public int size();

197

198

// Clock control

199

200

/** Get current Looper time */

201

public long getCurrentTime();

202

203

// Looper state control

204

205

/** Pause Looper (tasks won't execute automatically) */

206

public void pause();

207

208

/** Unpause Looper (tasks execute when posted) */

209

public void unPause();

210

211

/** Check if Looper is paused */

212

public boolean isPaused();

213

214

/** Reset Looper state */

215

public void reset();

216

}

217

```

218

219

**Usage Examples:**

220

221

```java

222

import static org.robolectric.Shadows.shadowOf;

223

224

// Main looper control

225

ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());

226

227

// Execute all pending tasks

228

mainLooper.idle();

229

230

// Execute tasks for 5 seconds

231

mainLooper.idleFor(Duration.ofSeconds(5));

232

233

// Check for pending work

234

if (mainLooper.hasQueuedTasks()) {

235

mainLooper.runOneTask();

236

}

237

238

// Background looper control

239

HandlerThread backgroundThread = new HandlerThread("background");

240

backgroundThread.start();

241

ShadowLooper backgroundLooper = shadowOf(backgroundThread.getLooper());

242

backgroundLooper.idle();

243

```

244

245

### SystemClock Control

246

247

Clock manipulation for time-sensitive testing in PAUSED and INSTRUMENTATION_TEST modes.

248

249

```java { .api }

250

/**

251

* Shadow for SystemClock providing clock control in tests.

252

*/

253

public class ShadowSystemClock {

254

/** Advance system clock by specified amount */

255

public static void advanceBy(Duration duration);

256

public static void advanceBy(long time, TimeUnit timeUnit);

257

258

/** Set absolute system time */

259

public static void setCurrentTimeMillis(long millis);

260

261

/** Get current system time */

262

public static long currentTimeMillis();

263

264

/** Sleep current thread for specified time without advancing clock */

265

public static void sleep(Duration duration);

266

public static void sleep(long time, TimeUnit timeUnit);

267

}

268

```

269

270

**Usage Example:**

271

272

```java

273

// Start with known time

274

ShadowSystemClock.setCurrentTimeMillis(1000000000L);

275

276

// Advance time by 1 hour

277

ShadowSystemClock.advanceBy(Duration.ofHours(1));

278

279

// Verify time-based behavior

280

assertThat(System.currentTimeMillis()).isEqualTo(1000000000L + 3600000L);

281

```

282

283

### Handler and Message Control

284

285

Fine-grained control over Handler and Message execution.

286

287

```java { .api }

288

/**

289

* Shadow for Handler providing message control.

290

*/

291

public class ShadowHandler {

292

/** Get ShadowHandler for specific Handler */

293

public static ShadowHandler shadowOf(Handler handler);

294

295

// Message queue inspection

296

297

/** Check if Handler has pending messages */

298

public boolean hasMessages(int what);

299

public boolean hasMessages(int what, Object object);

300

301

/** Get queued messages */

302

public List<Message> getMessages();

303

304

// Message execution control

305

306

/** Execute all queued messages */

307

public void flush();

308

309

/** Execute specific message */

310

public void handleMessage(Message message);

311

}

312

```

313

314

## Threading Patterns by Mode

315

316

### PAUSED Mode (Recommended)

317

318

```java

319

@LooperMode(LooperMode.Mode.PAUSED)

320

public class PausedModeTest {

321

@Test

322

public void testAsyncOperation() {

323

// Start async operation

324

myService.performAsyncOperation();

325

326

// Tasks are queued but not executed automatically

327

ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());

328

assertThat(mainLooper.hasQueuedTasks()).isTrue();

329

330

// Explicitly execute tasks

331

mainLooper.idle();

332

333

// Verify result

334

assertThat(myService.isOperationComplete()).isTrue();

335

}

336

337

@Test

338

public void testDelayedOperation() {

339

// Post delayed task (5 seconds)

340

Handler handler = new Handler(Looper.getMainLooper());

341

handler.postDelayed(() -> myCallback.onComplete(), 5000);

342

343

// Advance time and execute

344

ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());

345

mainLooper.idleFor(Duration.ofSeconds(5));

346

347

// Verify callback executed

348

verify(myCallback).onComplete();

349

}

350

}

351

```

352

353

### INSTRUMENTATION_TEST Mode

354

355

```java

356

@LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)

357

public class InstrumentationTest {

358

@Test

359

public void testWithSeparateTestThread() {

360

// Test runs on separate thread from main Looper

361

// Can use androidx.test APIs that expect this pattern

362

363

// Use CountDownLatch for synchronization

364

CountDownLatch latch = new CountDownLatch(1);

365

366

// Post to main thread

367

new Handler(Looper.getMainLooper()).post(() -> {

368

// UI operations on main thread

369

myActivity.updateUI();

370

latch.countDown();

371

});

372

373

// Wait for completion

374

latch.await(5, TimeUnit.SECONDS);

375

376

// Verify result

377

assertThat(myActivity.getDisplayText()).isEqualTo("Updated");

378

}

379

}

380

```

381

382

### Legacy Mode (Not Recommended)

383

384

```java

385

@SuppressWarnings("deprecation")

386

@LooperMode(LooperMode.Mode.LEGACY)

387

public class LegacyModeTest {

388

@Test

389

public void testWithScheduler() {

390

// Use deprecated scheduler APIs

391

Scheduler scheduler = Robolectric.getForegroundThreadScheduler();

392

393

// Control execution

394

scheduler.pause();

395

myService.performAsyncOperation();

396

397

// Manually advance

398

scheduler.advanceToLastPostedRunnable();

399

400

// Verify result

401

assertThat(myService.isOperationComplete()).isTrue();

402

}

403

}

404

```

405

406

## Best Practices

407

408

### Migration from LEGACY to PAUSED

409

410

1. **Replace Scheduler APIs**:

411

```java

412

// Old (LEGACY)

413

Robolectric.getForegroundThreadScheduler().advanceToLastPostedRunnable();

414

415

// New (PAUSED)

416

shadowOf(Looper.getMainLooper()).idle();

417

```

418

419

2. **Handle Background Threads**:

420

```java

421

// Old (LEGACY) - everything on main thread

422

Robolectric.getBackgroundThreadScheduler().advanceToLastPostedRunnable();

423

424

// New (PAUSED) - real background threads

425

HandlerThread thread = new HandlerThread("background");

426

thread.start();

427

shadowOf(thread.getLooper()).idle();

428

```

429

430

3. **Time Management**:

431

```java

432

// Old (LEGACY)

433

scheduler.advanceBy(5000);

434

435

// New (PAUSED)

436

ShadowSystemClock.advanceBy(Duration.ofSeconds(5));

437

shadowOf(Looper.getMainLooper()).idle();

438

```

439

440

### Test Cleanup

441

442

Always ensure proper cleanup to avoid test pollution:

443

444

```java

445

@After

446

public void cleanup() {

447

// Reset looper state

448

shadowOf(Looper.getMainLooper()).reset();

449

450

// Clean up background threads

451

if (backgroundThread != null) {

452

backgroundThread.quitSafely();

453

}

454

}

455

```

456

457

### Debugging Tips

458

459

1. **Check for Unexecuted Tasks**:

460

```java

461

ShadowLooper mainLooper = shadowOf(Looper.getMainLooper());

462

if (mainLooper.hasQueuedTasks()) {

463

System.out.println("Unexecuted tasks: " + mainLooper.size());

464

}

465

```

466

467

2. **Inspect Next Task Time**:

468

```java

469

long nextTaskTime = mainLooper.getNextScheduledTaskTime();

470

System.out.println("Next task at: " + nextTaskTime);

471

```

472

473

3. **Use Incremental Execution**:

474

```java

475

// Execute one task at a time for debugging

476

while (mainLooper.runOneTask()) {

477

// Inspect state after each task

478

}

479

```

480

481

## Common Issues and Solutions

482

483

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

484

485

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

486

487

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

488

489

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