How 40 Lines of Code Sped Up iOS End to End Tests by over 50%

March 17, 2025 ,

At Wealthfront, testing is one of our core engineering principles—in the form of unit and end-to-end testing on all platforms. The iOS team in particular manages its own CI infrastructure to run tests against an integration server mirroring our production environment. With roughly 30k unit tests and nearly 1k iOS end-to-end tests (and counting), speeding up our E2E suite has become a top priority as it takes a substantial amount of time to run. In this post, we’ll highlight how we sped up our tests by 50% with a small, targeted change.

The Challenge: Slow and Flaky UI Tests

iOS E2E tests have long been a headache. XCUITest and xcodebuild are notorious for flakes and hangs, and when tests aren’t written to guard against these issues, speed and reliability suffers significantly. Despite optimizations like disabling animations, enabling parallel testing, and sharding tests across multiple Mac Minis, our E2E test suite was taking a substantial amount of time to complete—even when running concurrently on three machines.

We’ve found there are two main strategies when it comes to iOS E2E tests with XCUITest, we always prioritize reliability:

  1. Prioritizing Speed over Reliability: By doing immediate assertions, e.g. XCTAssertTrue(self.testApp.textFields[“email”].exists), speed is increased substantially, but this doesn’t scale well in parallelized environment, particularly on older machines
  2. Prioritizing Reliability over Speed: By waiting for assertions to be true, e.g. self.testApp.textFields[“email”].waitForExistence(), speed is decreased substantially, but scales better in parallelized environments.

As we’ve scaled over time to include more and more E2E tests, we’ve leaned into the latter strategy using XCTWaiter to poll for conditions to be satisfied, unless we are absolutely certain that an element will immediately meet whatever predicate we’re asserting. The impact this has on test time can be seen in the timed log provided by xcode.

Here’s a snippet from one of our recurring withdrawal flow tests:

Notice any bottlenecks? 

Something that caught our eye is the 1s between each query. After some investigation we found that XCTWaiter’s polling mechanism (which is immutable and unconfigurable) waits 1 second between checks—blocking the test runner’s process run loop until a condition is met or the timeout is reached. This design is clearly dated, originating from an era before M-series macs.

A Quick Fix: Faster Polling with a Custom XCTWaiter Replacement

Since Apple has close-sourced its implementation, we decided to recreate and improve this mechanism from scratch. Our first iteration involved polling every 0.1 seconds instead of 1 second. We also added a pre-check to bypass polling if the condition was already true from the very start. Here’s our initial implementation:

After updating our helper functions to use this new mechanism, the difference was immediate:

BEFORE
AFTER

While the local speed increase of 80% was promising, these improvements weren’t reflected in our CI. We saw quite a few problems when running in a parallelized environment, likely due to the additional CPU overhead this new polling mechanism introduced. Polling 10x more frequently across four concurrent processes overwhelmed our M1 Mac Minis, leading to occasional test process crashes and negating most if not all speed improvements.

Optimizing for Parallel Environments with Exponential Backoff

To tackle this, we refined our implementation with exponential backoff. This adjustment ensures that our polling mechanism adapts to both the condition’s responsiveness and the CPU load on our CI nodes. The final implementation looked like this:

With these adjustments, our CI pipelines showed a dramatic reduction in execution time while remaining stable:

Final Thoughts

By taking the time to observe and investigate bottlenecks in our E2E test suite, we achieved:

  • Faster Execution Times: Up to 80% speed improvement locally and 50% on our CI pipelines.
  • Future-Proofing: Our tests will now automatically take advantage of hardware improvements without requiring any code changes.

Our experience underscores a critical lesson: often, the biggest bottlenecks can be solved with the simplest tweaks—if you just take the time to observe the problem. Sometimes, a fresh perspective and a small implementation change can yield enormous benefits.

Appendix – Integrating into Your iOS E2E test Suite

If you wish to implement this polling mechanism in your own test suite, we recommend creating some helper functions that extend XCUIElement. Here’s some examples:


Disclosures

The information contained in this communication is provided for general informational purposes only, and should not be construed as investment or tax advice. Nothing in this communication should be construed as a solicitation or offer, or recommendation, to buy or sell any security. Any links provided to other server sites are offered as a matter of convenience and are not intended to imply that Wealthfront or its affiliates endorses, sponsors, promotes and/or is affiliated with the owners of or participants in those sites, or endorses any information contained on those sites, unless expressly stated otherwise.

All UI screenshots provided are for illustrative and educational purposes only and any performance figures displayed should not be considered representative of actual performance.

Investment advisory services are provided by Wealthfront Advisers LLC, an SEC-registered investment adviser. Brokerage services are provided by Wealthfront Brokerage LLC, Member FINRA/SIPC. Financial planning tools are provided by Wealthfront Software LLC.

All investing involves risk, including the possible loss of money you invest, and past performance does not guarantee success. Please see our Full Disclosure for important details.

Wealthfront Advisers LLC, Wealthfront Brokerage LLC, and Wealthfront Software LLC are wholly owned subsidiaries of Wealthfront Corporation.

© 2025 Wealthfront Corporation. All rights reserved.