Faster Feedback: Benefits and Challenges of Parallelizing Unit Test Automation in iOS Apps

Unit Test Automation Apr 4, 2025

At Halodoc, we continuously strive to enhance our development processes by adopting the latest advancements in technology to improve performance, scalability and efficiency. One critical aspect of maintaining a robust iOS app is ensuring fast and reliable test automation. As our codebase grows, so does the need for efficient unit testing to catch issues early without slowing down development cycles. Parallelizing unit test execution is a game-changer in achieving faster feedback loops. In this blog post, we’ll explore how we implemented parallelized unit testing.


The Problem: Slow Feedback Loops

In a fast-paced development environment like Halodoc, where we frequently release new features and updates, slow feedback loops can be a significant bottleneck. Our iOS project is a multi-module application with a growing test suite. Initially, all our unit tests were executed sequentially, which meant that every test had to wait for the previous one to complete. This approach worked fine when the test suite was small, but as the project grew, so did the test execution time.

Our unit test suite ran sequentially, leading to slow feedback loops and affecting developer efficiency. Unit test is one of multiple key metric to ensure quality delivery which was creating resistance, leading to longer development cycles and slower releases. This bottleneck affected collaboration, increased context switching, and made it harder to maintain development momentum. Parallelizing tests significantly improved efficiency, enabling faster iterations and quicker feedback loops.


Why Faster Feedback Matters in iOS Development

Fast feedback loops are essential for maintaining high development velocity. In a CI/CD pipeline, the time taken for tests to run directly impacts the speed at which developers can push their changes and deploy updates. Long-running test suites slow down development, leading to bottlenecks and impact release timelines.

By parallelizing test execution, we have achieved a ~25% reduction in unit test execution time, allowing developers to receive immediate feedback on their changes more efficiently.. This results in:

  • Improved Developer Productivity: Engineers can iterate quickly without waiting for long test runs, making the development process more seamless and reducing overall frustration.
  • Faster Feedback Loop: Faster feedback ensures that issues are detected and resolved early in the development cycle, preventing defects from propagating into production and reducing debugging effort later on.
  • Scalability: As the test suite grows with new features and functionalities, parallel execution helps maintain reasonable test durations, ensuring that adding more tests does not lead to exponentially longer test runs.
  • Better CI/CD Performance: Optimising test execution time enables continuous integration pipelines to complete faster, improving deployment efficiency and reducing time-to-market for new features. Its will also increase Availability as overall time to run job will decreased.

Without parallelization, productivity of developers reduces as test suites are not time efficient. Shortening test execution times helps maintain a rapid iteration cycle, allowing teams to focus more on feature development and less on waiting for test results.

Additionally, reducing test execution time enhances collaboration between developers, QA teams, and product managers. With quicker feedback, teams can make informed decisions faster, improving overall workflow efficiency and ensuring that releases remain on schedule.


What is Test Parallelization?

Test parallelization is the process of running multiple unit tests simultaneously instead of executing them sequentially. By distributing test execution across multiple processes, test parallelization significantly reduces test runtime, speeds up feedback loops, and enhances CI/CD efficiency.

How It Works?

  1. Test Suite Breakdown – The test runner analyzes and groups test cases that can run independently.
  2. Parallel Execution – Tests are executed in parallel across multiple process, reducing overall runtime.
  3. Results Aggregation – Once all tests complete, results are collected and merged into a final report.


How We Parallelized Unit Testing at Halodoc

Leveraging XCTest Parallelization

iOS provides built-in support for parallel test execution using XCTest. We utilized XCTest's ability to distribute test execution across multiple threads, reducing overall test runtime.

Key steps taken:

  • Enabling Parallel Execution: XCTest allows tests to be executed in parallel by setting -parallel-testing-enabled YES in the test configuration.

Optimizing Test Structure for Parallel Execution

To ensure smooth parallel execution, we had to structure our tests to be independent and stateless. Some optimizations included:

1. FIRST principle: To maximize the benefits of parallel execution, unit tests should follow the FIRST principle:

  • Fast – Tests should execute quickly to provide rapid feedback. Slow tests can bottleneck parallel execution.
  • Isolated – Each test should run independently, avoiding shared states and dependencies that could cause flakiness in a parallel environment.
  • Repeatable – Tests should produce consistent results every time they run, regardless of the order or environment.
  • Self-validating – A test should have a clear pass/fail outcome without requiring manual verification.
  • Timely – Write tests early in the development cycle to catch issues before they reach production.

2. Avoiding Shared State: Tests should not depend on a global or shared state, ensuring they can run in isolation.

3. Mocking Dependencies: We replaced real dependencies with mocks to eliminate conflicts.

4. Minimizing Resource Contention: Ensuring test cases do not access the same file or database concurrently.

Common Shared Resource Issues and Their Solutions

Lets discuss this with example in more detail.

Direct Access to File System (Shared Resource)

Root Cause Analysis

  • Shared File Access: All tests interact with logs.txt, creating a race condition where multiple tests might try to write simultaneously, leading to corrupt or incomplete logs.
  • Non-Deterministic Behavior: If multiple test cases log messages in parallel, log order may change, causing intermittent test failures.
  • Difficult Cleanup: Each test has to manually delete or reset the file, which is error-prone and slows down execution.
  • Slow Execution: Writing to disk is significantly slower than in-memory operations, impacting test performance.

Correction Strategy : Using Dependency Injection for File Logging

No Shared File Access: AnalyticsManager now receives a Logging instance via dependency injection, eliminating file contention issues.

  • Each Test Uses a Mock Instance: Instead of writing to a file, tests use an in-memory MockLogger, ensuring complete isolation.
  • No Race Conditions: Since each test has its own MockLogger, multiple tests can log messages without interfering with each other.
  • Faster Execution: Logging to memory (MockLogger) is instant, making tests much faster compared to file I/O.
  • Deterministic & Reliable Tests: The test result does not depend on log order or file access delays, preventing flakiness.

Direct Dependency on a Singleton (Shared Resource)

Root Cause Analysis

  • Shared State Contamination: Since ConfigManager.shared is a singleton, if one test modifies apiURL, another test running in parallel may read an incorrect value, causing test failures.
  • Race Conditions: Tests modifying the same singleton simultaneously can lead to unexpected behavior and flaky tests that sometimes pass and sometimes fail.
  • Difficult to Mock: Singletons tightly couple the code, making it hard to substitute them with test-specific values.
  • Unreliable Parallel Execution: Since all tests share the same ConfigManager.shared, one test can unintentionally break another, leading to unpredictable failures.

Correction Strategy: Using a Protocol and Dependency Injection

  • No Singletons: The APIClient no longer relies on ConfigManager.shared, eliminating shared state problems.
  • Independent Instances: Each test gets its own instance of ConfigManager (or MockConfigManager), ensuring complete isolation.
  • No Race Conditions: Since each test provides its own configManager, parallel tests never interfere with each other.
  • Easier Mocking: We can now inject different implementations (e.g., MockConfigManager) for testing, making it cleaner and more flexible.
  • Faster Execution: Since tests don’t need to reset shared state, they run more efficiently in parallel.

Tips and Tricks for Writing Faster Unit Tests

Parallelizing unit tests is only part of the solution. Writing efficient test cases ensures that they run quickly and reliably. To enhance speed and efficiency while maintaining parallel execution, adhere to the FIRST principle we talked earlier.

Here are some best practices:

Write Precise and Small Unit Tests

  • Each test should focus on a single piece of logic.
  • Avoid testing multiple behaviors in a single test case.

Avoid Integration Testing in Unit Tests

  • Unit tests should not depend on network calls, database queries, or file system operations unless necessary.
  • Use mocks and fakes to simulate dependencies.

Minimize Setup Overhead

  • Heavy test setup slows down execution. Optimize setup methods to only include what’s necessary.
  • Prefer lazy initialization over setting up every test case.

Use Parallelization-Friendly Code Design

  • Design your code in a way that naturally supports independent execution.
  • Dependency injection helps ensure each test has isolated dependencies.

Keep Test Execution Time Under Control

  • Regularly review slow tests and optimize or break them down.
  • Aim for each test to run in milliseconds rather than seconds.

Use Test Isolation Techniques

  • Ensure tests do not modify shared global state.
  • Leverage dependency injection to create unique test instances.

Monitor and Address Flaky Tests

  • Identify and fix tests that pass inconsistently.
  • Use test retries sparingly to avoid masking underlying issues.

Limitations of Parallelization Testing

While parallelizing unit tests improves execution speed, it introduces several challenges that can impact reliability and maintainability:

Debugging Becomes More Challenging

  • Logs from multiple parallel test runs can get interleaved, making it difficult to trace failures and identify root causes.

Higher Infrastructure and CI/CD Complexity

  • Configuring parallel test execution in CI/CD pipelines requires additional resources and careful setup to distribute test workloads effectively.

Higher Resource Utilization

  • Running tests in parallel consumes more CPU and memory, which can lead to resource contention, especially in limited test environments.

Conclusion

Parallelizing unit test automation has significantly reduction in unit test execution time, by ~25% . While it introduced challenges, optimizing test structure and managing shared resources helped us unlock faster feedback loops and a more scalable testing strategy. As we continue to evolve, we will keep refining our testing approach to enhance performance and maintain high-quality standards.

If you're looking to implement parallel test execution in your iOS projects, start with XCTest’s built-in capabilities, focus on test independence, and fine-tune your test structure. Faster tests lead to faster innovation, and in a fast-moving development environment, every second counts!

References

Here are some useful references related to parallel unit testing, dependency injection, and avoiding shared resource conflicts in iOS development:

  1. Go further with Swift Testing
  2. Testing Tips & Tricks
  3. Get your test results faster

These references will help in understanding best practices and improving test reliability while running unit tests in parallel.

Join us

Scalability, reliability, and maintainability are the three pillars that govern what we build at Halodoc Tech. We are actively looking for engineers at all levels and if solving hard problems with challenging requirements is your forte, please reach out to us with your resumé at careers.india@halodoc.com.

About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek, Astra, Temasek, and many more. We recently closed our Series D round and In total have raised around USD$100+ million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.

Tags

Vikesh Prasad

Software Development Engineer iOS