In our series on unlocking the full potential of Bitrise build pipelines, we now turn our attention to iOS developers: how to run tests in parallel. This strategy is not only about accelerating feedback cycles but also about intelligently executing tests only for the components that have changed. Tailored for highly modularized iOS applications, this method ensures rapid, targeted testing without compromising on code quality.
What to expect in this series:
- Blog post 1: How to share files with workflows in a pipeline on Bitrise
- Blog post 2: How to conditionally run workflows of a Bitrise pipeline
- Blog post 3: How to configure your pipeline stages to save credits
- Blog post 4: How to run iOS tests in parallel (you are here)
Why parallel test execution matters
In a mobile development landscape where speed and efficiency are paramount, parallelizing unit tests across different modules of an iOS application can significantly reduce wait times for test results. By focusing on changed targets, developers gain faster insights, leading to quicker iterations and ultimately, a more agile development process.
Setting up your pipeline for parallel testing: A step-by-step guide
Step 1: Creating your pipeline structure
Begin with defining a two-stage pipeline: the first stage is to identify changes and plan executions, and the second is to carry out the tests on the affected modules. This foundational setup is crucial for the dynamic execution plan.
pipelines:
build:
stages:
- setup-builds: {}
- build-modules: {}
Step 2: Configuring conditional workflows
Each module of your application, such as App, Home, Search, and Profile, gets its dedicated workflow within the build-modules stage, with conditions set to run tests based on specific environmental variables. This setup ensures that only relevant tests are executed, saving time and resources. As the build-modules stage executes the tests. It should have as many workflows as many modules the application has. In this example, the app has 4 modules:
- App: The main target which glues together every other module
- Home: Contains everything related to the home screen (views, models, actions, …)
- Search: Holds every component related to the search functionality of the app
- Profile: Everything profile related
stages:
setup-builds:
workflows:
- export-changed-components: {}
build-modules:
workflows:
- build-module-app:
run_if: '{{ enveq "MODULE_APP" "true" }}'
- build-module-home:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_HOME" "true") }}'
- build-module-search:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_SEARCH" "true") }}'
- build-module-profile:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_PROFILE" "true") }}'
Let’s deep dive into this for a second.Â
The build-module-app workflow has the following run_if condition:
'{{ enveq "MODULE_APP" "true" }}'
This means that the workflow will be executed only if the MODULE_APP env var exists and its value is true. Otherwise, the workflow will be skipped.
The other three run_if  are very similar. Let’s dive into one of them: Â
'{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_HOME" "true") }}'
This is a more complex expression with two conditions.Â
- The first one is that the MODULE_APP env var does not exists. We achieve this by comparing it to an empty value.Â
- The second part is for checking if the MODULE_HOME env var is set to true. The workflow will not be executed if one or both condition is not true.
Step 3: Detecting changes and setting variables
This step involves implementing a workflow to detect changes in modules and set corresponding environmental variables. This crucial step involves scripting to identify modified components, utilizing tools like git diff to check for changes, thereby enabling conditional execution of subsequent workflows.
Let’s look at the below code snippet. The export-changed-components workflow is responsible for figuring out what module tests should be executed. The current implementation is incomplete as the appModuleChanged, homeModuleChanged, searchModuleChanged and profileModuleChanged functions are dummy functions. These need to be adapted to the project structure. For example, one implementation could be to execute git diff main --name-only and check the paths.
If a module has changes, then an env var will be set with the value true. For example, the Home module has the MODULE_HOME env var.
The env vars then have to be shared with the following workflows, which form part of `build-modules`. Thus, onto Step 4.
workflows:
export-changed-components:
steps:
- script@1:
inputs:
- content: |-
#!/usr/bin/env bash
set -e
set -o pipefail
set -x
appModuleChanged() {
false
}
homeModuleChanged() {
return
}
searchModuleChanged() {
return
}
profileModuleChanged() {
return
}
if appModuleChanged; then
envman add --key MODULE_APP --value true
fi
if homeModuleChanged; then
envman add --key MODULE_HOME --value true
fi
if searchModuleChanged; then
envman add --key MODULE_SEARCH --value true
fi
if profileModuleChanged; then
envman add --key MODULE_PROFILE --value true
fi
- share-pipeline-variable@1:
inputs:
- variables: |-
MODULE_APP
MODULE_HOME
MODULE_SEARCH
MODULE_PROFILE
Step 4: Executing module-specific tests
Organize your workflows to run tests for each module based on the scheme name, with a shared build workflow (which has all of the steps for running the tests) for actual test execution. You can then create as many wrapper workflows as many modules as the project has. These wrapper workflows aim to set the SCHEME env var and then execute the build workflow. This will make sure that the build workflow always executes the correct tests. This approach simplifies your pipeline and ensures that tests for each module are run only when needed, optimizing the testing phase.
workflows:
build-module-app:
envs:
- SCHEME: App
after_run:
- build
build-module-home:
envs:
- SCHEME: Home
after_run:
- build
build-module-profile:
envs:
- SCHEME: Profile
after_run:
- build
build-module-search:
envs:
- SCHEME: Search
after_run:
- build
build:
steps:
- git-clone@8: {}
- xcode-test@5:
inputs:
- scheme: "$SCHEME"
- deploy-to-bitrise-io@2: {}
YML Configuration
The provided YML configuration snippet lays out the structure for your pipeline, from stages and workflows to environment variables and test execution, offering a ready-to-implement template for your project.
---
format_version: '13'
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
project_type: ios
pipelines:
build:
stages:
- setup-builds: {}
- build-modules: {}
stages:
setup-builds:
workflows:
- export-changed-components: {}
build-modules:
workflows:
- build-module-app:
run_if: '{{ enveq "MODULE_APP" "true" }}'
- build-module-home:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_HOME" "true") }}'
- build-module-search:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_SEARCH" "true") }}'
- build-module-profile:
run_if: '{{ (getenv "MODULE_APP" | eq "") | and (enveq "MODULE_PROFILE" "true") }}'
workflows:
build-module-app:
envs:
- SCHEME: App
after_run:
- build
build-module-home:
envs:
- SCHEME: Home
after_run:
- build
build-module-profile:
envs:
- SCHEME: Profile
after_run:
- build
build-module-search:
envs:
- SCHEME: Search
after_run:
- build
build:
steps:
- git-clone@8: {}
- xcode-test@5:
inputs:
- scheme: "$SCHEME"
- deploy-to-bitrise-io@2: {}
export-changed-components:
steps:
- script@1:
inputs:
- content: |-
#!/usr/bin/env bash
set -e
set -o pipefail
set -x
appModuleChanged() {
false
}
homeModuleChanged() {
return
}
searchModuleChanged() {
return
}
profileModuleChanged() {
return
}
if appModuleChanged; then
envman add --key MODULE_APP --value true
fi
if homeModuleChanged; then
envman add --key MODULE_HOME --value true
fi
if searchModuleChanged; then
envman add --key MODULE_SEARCH --value true
fi
if profileModuleChanged; then
envman add --key MODULE_PROFILE --value true
fi
- share-pipeline-variable@1:
inputs:
- variables: |-
MODULE_APP
MODULE_HOME
MODULE_SEARCH
MODULE_PROFILE
app:
envs:
- BITRISE_PROJECT_PATH: App.xcworkspace
opts:
is_expand: false
meta:
bitrise.io:
stack: osx-xcode-15.0.x
machine_type_id: g2-m1.4core
Bitrise Steps highlighted
- Script: For detecting changes and setting environmental variables.
- Share Pipeline Variable: To pass variables across workflows.
- Git Clone: Ensures your codebase is up-to-date before testing.
- Xcode Test: Executes the tests within Xcode projects.
- Deploy to Bitrise.io: Shares test results and artifacts.
ConclusionÂ
This approach to running iOS tests in parallel on Bitrise represents a significant leap for teams seeking efficiency in their development cycles. By focusing test execution on changed modules, you achieve faster feedback loops without sacrificing the integrity of your application.
As we look ahead, we will explore more ways to refine and enhance your CI/CD workflows with Bitrise. If you're ready to implement parallel testing in your projects, dive into the detailed Bitrise Pipelines documentation for comprehensive guides and tutorials. Not a Bitrise user? Start for free today.