How to run iOS tests from the terminal

Running iOS tests from the command line shouldn’t require memorizing xcodebuild’s destination strings, SDK flags, and test filtering syntax. This post covers why xcodebuild test is hostile to automation and how FlowDeck replaces it with one command, structured NDJSON output, and live progress.

The typical xcodebuild invocation looks like this:

xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.4' \
  -only-testing:MyAppTests/LoginTests/testValidLogin

Get the destination string wrong and it fails silently. Forget -sdk iphonesimulator and it tries to build for a device you don’t have. The output is hundreds of lines of mixed build logs and test results with no structure. Every guide on this topic, including Apple’s own documentation, shows the same wall of flags from 2020.

There’s a simpler way.

The xcodebuild problem

xcodebuild test works. It’s also hostile to automation.

The destination string is fragile. platform=iOS Simulator,name=iPhone 16,OS=18.4 has to match exactly. If you upgraded Xcode and the OS version changed, the string breaks. If the simulator name doesn’t match, you get a generic “unable to find a destination” error with no suggestion of what’s available.

Filtering tests requires knowing the exact format: TargetName/ClassName/testMethodName. There’s no discovery command. You either know the path or you open Xcode and look.

The output mixes build progress, compiler warnings, test results, and framework logs into one stream. Parsing this in CI means regex. Regex means it breaks when Apple changes the format.

And there’s no progress. Tests run in silence until everything finishes or something crashes.

The FlowDeck way

flowdeck test -S "iPhone 16"

That’s it. FlowDeck resolves the workspace, scheme, and simulator by name. No destination strings. No UDID lookups. No -sdk flag.

Run specific tests

# One test method
flowdeck test --only MyAppTests/LoginTests/testValidLogin

# One test class
flowdeck test --only MyAppTests/LoginTests

# Skip slow tests
flowdeck test --skip MyAppTests/PerformanceTests

Same filtering syntax as xcodebuild, but without the 6 other flags around it.

Discover tests first

flowdeck test discover

Lists every test target, class, and method in your project. FlowDeck uses AST parsing instead of building the entire project, which makes discovery roughly 100x faster than xcodebuild build-for-testing. Useful when you inherit a project and don’t know what’s testable, or when your agent needs to pick which tests to run.

Watch progress

flowdeck test --progress

Shows test results as they complete. Pass, fail, skip. Not 200 lines of silence followed by a summary.

Structured output for CI

flowdeck test --json

Every test result is a typed NDJSON event:

{"type":"test_passed","target":"MyAppTests","class":"LoginTests","method":"testValidLogin","duration":0.42}
{"type":"test_failed","target":"MyAppTests","class":"LoginTests","method":"testExpiredToken","message":"XCTAssertEqual failed: expected 401, got 200","file":"LoginTests.swift","line":87}
{"type":"result","success":false,"passed":14,"failed":1,"skipped":2,"duration":8.3}

No regex. No xcpretty. No xcbeautify. Parse it with jq, pipe it into your CI reporter, or let your AI agent read it directly.

GitHub Actions

- name: Test
  run: |
    flowdeck test --json | tee test-results.json
    # Fail the job if tests failed
    tail -1 test-results.json | jq -e '.success == true'

What this is and isn’t

FlowDeck runs your existing XCTest and Swift Testing tests. It calls xcodebuild under the hood. It doesn’t replace your test framework or require any changes to your test code.

It does not generate test coverage reports. Use xcodebuild -enableCodeCoverage YES -resultBundlePath for that. FlowDeck handles the build and execution. Coverage parsing is a separate concern.

Native Swift. Runs locally. No telemetry.

Q&A

Can I run a single XCUITest from the command line?

Yes. Use flowdeck test --only TargetName/ClassName/methodName.

Same filter syntax as xcodebuild’s -only-testing flag, without the six other flags around it. Filters work for unit tests and UI tests identically.

How do I get structured JSON output from xcodebuild test?

xcodebuild doesn’t produce structured output natively. Common workarounds parse the .xcresult bundle after the run, but that adds a build step.

flowdeck test --json emits NDJSON events live. One event per test pass, fail, or skip, plus a final result summary.

Pipe it to jq or any CI reporter.

Why is xcodebuild test discovery so slow?

Because xcodebuild build-for-testing actually compiles the project to enumerate tests.

FlowDeck uses AST parsing instead, reading Swift source files directly to find XCTestCase subclasses and @Test functions.

That’s typically 50 to 100 times faster on a real project.

Does FlowDeck work with both XCTest and Swift Testing?

Yes. AST discovery recognizes XCTestCase subclasses, XCTest method names, and Swift Testing @Test annotations.

Run filters use the same target/class/method path regardless of framework. NDJSON events include the framework type so you can distinguish them in CI.

How do I run iOS tests in GitHub Actions?

Pipe flowdeck test --json to a results file and check the final result event for success. The example in this post is a working starting point.

For runtime install and caching, FlowDeck handles both via flowdeck simulator runtime install. See docs.flowdeck.studio/cli/testing.