GitLab has a Continuous Integration (CI) feature that allows you to perform continuous integration without deploying a separate solution dedicated to CI.
Therefore, whenever I push my code to GitLab, XCTest is triggered to run unit tests for macOS and iOS apps.
In this article, I’ll walk you through setting this up.
Register the runners
In GitLab CI/CD, a Runner runs the process. There are two types of Runners: the Shared Runner, which is shared between projects, and the Specific Runner, which is dedicated to a particular project.
For this instance, I registered a “Shared Runner” to use it across multiple projects(repositories).
Install a Runner
Since this is the Runner used to run XCTest, you need to install it on the Mac that will run XCTest. Here, I used a Mac Mini.
The official website explains instructions for installing GitLab Runner on macOS.
The binary to download is different for the Intel Mac and the Apple Silicon. Download in the terminal with curl
.
For Intel Mac:
sudo curl --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64"
For Apple Silicon:
sudo curl --output /usr/local/bin/gitlab-runner "https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-arm64"
This command will download the Runner into the /usr/local/bin/gitlab-runner
directory.
sudo chmod +x /usr/local/bin/gitlab-runner
cd ~
gitlab-runner install
You will need to open the ~/Library/LaunchAgents/gitlab-runner.plist
file, which is created by running the git-lab-runner install
command.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- omission -->
<key>EnvironmentVariables</key>
<dict>
<key>LC_ALL</key>
<string>en_US.UTF-8</string>
</dict>
</dict>
Normal users do not have read and write permissions for the default log file paths, causing the gitlab-runner
to fail upon execution. To make the log files readable and writable, change the value of the StandardOutPath
and StandardErrorPath
to change the log files’ path.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- omission -->
<key>StandardOutPath</key>
<string>/Users/USER_NAME/.gitlab-runner/gitlab-runner.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/USER_NAME/.gitlab-runner/gitlab-runner.err.log</string>
gitlab-runner start
Register a Runner
Register the Runner that you’ve installed on your Mac with GitLab.


When running XCTest on a Mac, you must run it in user mode, so you need to run without sudo
.
Once executed, you’ll be prompted to interactively enter those settings in the given order.
Runtime platform arch=amd64 os=darwin pid=10297 revision=76984217 version=15.1.0
Running in system-mode.
Enter the GitLab instance URL (for example, https://gitlab.com/):
[http://code.rkdev.corp/]:
Enter the registration token:
[-ABCDEFG4567890YZABC]:
Enter a description for the runner:
[MacMini2020.local]: MacMini2020
In my case:
- The machine is a Mac.
- It is an Intel Mac.
- Its OS will always be the latest.
Based on these factors, the following three tags were set.
- mac
- mac-intel
- mac-intel-latest
Enter tags for the runner (comma-separated):
mac,mac-intel,mac-intel-latest
Enter optional maintenance note for the runner:
The official website says to use shell
for macOS and iOS apps, so I specified shell
.
Registering runner... succeeded runner=-ABCDEFG
Enter an executor: custom, parallels, shell, ssh, kubernetes, docker, docker-ssh, virtualbox, docker+machine, docker-ssh+machine:
shell
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Configure the CI/CD
Create a repository and code for testing and set up the CI/CD.
Test Code
Here, I aim to verify that XCTest operates correctly via GitLab CI. Thus, I’ve constructed a unit test containing code designed to always fail and code designed to always succeed. I hesitate to call it a test code since we are not testing anything, just making XCTest work.
import XCTest
@testable import CITest
class CITestTests: XCTestCase {
override func setUpWithError() throws {
}
override func tearDownWithError() throws {
}
func testExample() throws {
XCTFail("TEST FAILURE")
}
func testSuccess() throws {
XCTAssertTrue(true);
}
}
Activate the Shared Runner
You must enable Shared Runner on a per-repository basis.

Add the CI/CD configuration file
Add the CI/CD configuration file by doing the following.


For example, I changed the following values in my test project file.
Configuration | Value |
---|---|
-project | CITest.xcodeproj |
-scheme | CITest |
-destination | iOS Simulator,name=iPhone 13,OS=15.5 |
tags | mac-intel-latest |
-archivePath | build/CITest |
-archivePath | build/CITest.xcarchive |
-exportPath | build/CITest.ipa |
-exportProvisioningProfile | “” |
paths: | build/CITest.ipa |
stages:
- build
- test
- archive
- deploy
build_project:
stage: build
script:
- xcodebuild clean -project CITest.xcodeproj -scheme CITest | xcpretty
- xcodebuild test -project CITest.xcodeproj -scheme CITest -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.5' | xcpretty -s
tags:
- mac-intel-latest
archive_project:
stage: archive
script:
- xcodebuild clean archive -archivePath build/CITest -scheme CITest
- xcodebuild -exportArchive -exportFormat ipa -archivePath "build/CITest.xcarchive" -exportPath "build/CITest.ipa" -exportProvisioningProfile ""
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
artifacts:
paths:
- build/CITest.ipa
tags:
- mac-intel-latest
If you only need testing by CI and do not need archiving, remove the archive_project
job and make the following configuration file. In my case, this is what I needed.
stages:
- build
- test
- archive
- deploy
build_project:
stage: build
script:
- xcodebuild clean -project CITest.xcodeproj -scheme CITest | xcpretty
- xcodebuild test -project CITest.xcodeproj -scheme CITest -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.5' | xcpretty -s
tags:
- mac-intel-latest
See the following article for details on specifying the -destination
option.

Test
Push the created test code to the repository. Since this is a must-fail test, the job will fail after a while, and you will receive an error notification email.
Open the Pipelines
of the CI/CD
of the project. You can see failed jobs. Click on FAILED
.

Open the Failed Jobs
tab. If the error is due to test failure as planned, the test case (method) name of the failed test is output as follows.
2022-06-27 18:59:30.903 xcodebuild[18902:109049] [MT] IDETestOperationsObserverDebug: 0.000 sec, +0.000 sec -- start
2022-06-27 18:59:30.903 xcodebuild[18902:109049] [MT] IDETestOperationsObserverDebug: 104.154 sec, +104.154 sec -- end
Failing tests:
CITestTests:
CITestTests.testExample()
** TEST FAILED **
ERROR: Job failed: exit status 1
If you are getting other errors, please review the logs and settings.
About the UI testing
Here we have only unit testing with XCTest, but it is no different for UI testing.
Suppose the UI Testing test package is included in the Xcode project and registered in the scheme’s test plan. In that case, UI testing will be performed automatically, just as when testing manually from Xcode.
Troubleshooting
I will post the errors I encountered when I set it up and how to handle them.
Only works when the user is logged in
XCTest must be run by the logged-in current user. Therefore, the Mac running the Runner must be logged in.
When the machine is rebooted
If you follow the official installation procedure, it will be registered as a service, so when you reboot your machine, all you have to do is log in, and Runner will start.
Status remains pending
The status is still pending
, and the pipeline build_project
shows an error that the Runner corresponding to the tag does not exist.
In this case, the following are possible causes.
- The
tag
is incorrect. - Try to use
Shared Runner
, butShared Runner
is not enabled in your project. - Runner on Mac is not running in user mode.
2 see “Activate Shared Runner” on this page.
I’m stuck on 3. When registering Runner, the command line output by GitLab has sudo
on it, and if I run it as is, Runner won’t work in user mode, so that XCTest won’t work either.
Simulator name or OS version is incorrect.
In this case, the following error is output in the build_project
log, which lists the available simulator and OS combinations.
xcodebuild: error: Unable to find a destination matching the provided destination specifier:
Correct the values you specify in the .gitlab-ci.yml
file to the appropriate values.
Open the Editor
of your project’s CI/CD
to edit and commit the .gitlab-ci.yml
file from within your web browser.
For information on how to specify the simulator name and OS version, see the following article.
