Understanding UI Thread Deadlocks in EarlGrey
Background and Integration Model
EarlGrey hooks into the app's run loop and waits for it to be idle before proceeding with assertions or interactions. It monitors network calls, animations, and dispatch queues to synchronize actions. However, if asynchronous work never reaches an idle state or bypasses EarlGrey's tracking, the framework stalls indefinitely.
Architectural Causes of Deadlocks
Common architectural patterns that contribute to hangs:
- Custom background queues not declared as tracked queues.
- Infinite Combine publishers keeping the run loop alive.
- DispatchGroup.wait() on the main thread causing hard locks.
- Long-running animations not accounted for in idle detection.
Diagnosing Test Hangs and Timeouts
Recognizing the Symptoms
- Test logs show no assertion failure but freeze after an interaction call.
- Test duration exceeds default 30-second timeout.
- Xcode shows test "stuck" on UI interaction with no progress.
- System logs show Main Thread blocking or watchdog termination.
Step-by-Step Debugging
- Enable verbose logging: Set
GREYConfiguration.sharedInstance().setValue(true, forConfigKey: kGREYConfigKeyVerboseLogging)
early in your test setup. - Attach debugger breakpoints: Use symbolic breakpoints on
__pthread_kill
ordispatch_semaphore_wait
to catch deadlocks. - Use Instruments: Profile with Time Profiler to detect threads blocked on main queue or semaphores.
- Check for Combine or RxSwift side effects: Look for never-completing subscriptions or missing cancellation tokens.
// Debug example: finding blocked DispatchGroup DispatchQueue.main.async { let group = DispatchGroup() group.enter() group.wait() // BAD: Blocks main thread and deadlocks EarlGrey }
Fixing Synchronization and Deadlock Issues
Register Custom Synchronization Resources
If your app uses non-standard queues or async patterns, inform EarlGrey explicitly:
let customQueue = DispatchQueue(label: "com.example.sync") GREYConfiguration.sharedInstance().setValue([customQueue], forConfigKey: kGREYConfigKeyTrackedDispatchQueues)
Break Cycles in Async Flows
- Ensure that Combine publishers and Rx chains always have completion handlers.
- Use test-specific schedulers that terminate after test expectations are fulfilled.
- Isolate animations with reduced durations or disable them altogether using
UIView.setAnimationsEnabled(false)
.
Fail Fast Instead of Hanging
Set shorter synchronization and interaction timeouts to expose hangs early:
GREYConfiguration.sharedInstance().setValue(5, forConfigKey: kGREYConfigKeyInteractionTimeoutDuration) GREYConfiguration.sharedInstance().setValue(10, forConfigKey: kGREYConfigKeySynchronizationTimeoutDuration)
Best Practices for Scalable EarlGrey Test Suites
- Mock external dependencies: Reduce reliance on real APIs using mock servers or local stubs.
- Audit main thread usage: Ensure async work doesn't re-enter the main queue unnecessarily.
- Use teardown handlers: Cancel long-running subscriptions or animations post-test.
- Apply test-specific configurations: Disable transitions, animation delays, and timers during UI tests.
- Integrate with CI properly: Ensure simulators are cleaned and reset between runs to avoid state pollution.
Conclusion
UI test hangs in EarlGrey are often not due to bugs in test code, but in the app's asynchronous architecture and lack of synchronization transparency. By diagnosing deadlocks, registering tracked queues, and refining test environments, developers can create robust and reliable UI test suites. EarlGrey remains a powerful tool—if used with architectural discipline.
FAQs
1. Why do EarlGrey tests hang without throwing errors?
Because EarlGrey waits for the run loop to idle, untracked or incomplete async operations can block progress silently.
2. Can EarlGrey handle Combine or RxSwift flows automatically?
Not by default. You need to register synchronization hooks or use test-friendly schedulers to ensure observables complete.
3. How can I simulate network responses in EarlGrey tests?
Use local HTTP stubs or dependency injection to return canned responses without relying on real network calls.
4. Should I disable animations in UI tests?
Yes, disabling animations reduces flakiness and improves test execution time. Use UIView.setAnimationsEnabled(false)
in test setup.
5. What are some reliable ways to catch deadlocks?
Use Time Profiler in Instruments, symbolic breakpoints on thread APIs, and shorten timeout settings to fail fast instead of hanging indefinitely.