# XCTest
URL: /docs/testing/xctest
LLM index: /llms.txt
Description: Gate CI on your unit and UI tests by running them on a cloud iOS simulator, with a result line per test case.

# Run XCTest suites

`lim xcode test` builds your scheme's test targets on a remote Xcode sandbox, runs them on an attached cloud iOS simulator (unit and UI targets alike), and streams one line per test case as it finishes. It exits non-zero when any test fails, so it drops straight into CI.

Everything about syncing from [Build with Xcode](/docs/ios/build-with-xcode) applies here too: XcodeGen projects, ignore and include patterns, additional files, and the sync cache.

## Run the suite

From the project directory, run the tests of the default scheme, or name the scheme explicitly:

```bash
lim xcode test .
lim xcode test ./MyProject --scheme MyApp
```

The command reuses your last simulator-backed Xcode target, or creates and attaches the sandbox and simulator it needs, so repeat runs skip provisioning and start with warm build state. The scheme must have a test action configured; shared schemes exported by Xcode have one whenever the project has test targets.

`--xcode-version 27` builds the test targets with another installed Xcode (`27` selects that major's newest GA release, `27.1` selects that exact version). The simulator keeps the fleet's default runtime, so the run warns that runtime-dependent failures are possible and then proceeds. See [Xcode and tool versions](/docs/ios/xcode-and-tools).

The output streams the build log first, then one line per case and a closing summary:

```text
  PASS LimFixtureTests.CartModelTests/testDiscountAppliesOverThreshold (12ms)
  FAIL LimFixtureTests.LegacyPricingTests/testLegacyRounding (31ms)
       XCTAssertEqual failed: ("$4.50") is not equal to ("$5.00")
  5 passed, 1 failed
```

Swift test classes are reported module-qualified (`Module.Class`), Objective-C classes bare.

## Select tests

Pass xcodebuild's identifier format `Target[/Class[/method]]`. Repeat a flag for several entries; `--only-testing` and `--skip-testing` are mutually exclusive:

```bash
# Only one test method
lim xcode test . --only-testing MyAppTests/LoginTests/testValidLogin

# Everything except the UI target
lim xcode test . --skip-testing MyAppUITests
```

A bare target name selects or skips that whole target. Targets named by no `--only-testing` entry are skipped entirely. An entry naming a target the build did not produce fails the run instead of silently running everything.

## Launch settings and attachments

Limrun preserves the launch arguments and environment recorded in the `.xctestrun` manifest, and keeps test-host and UI app settings separate: `CommandLineArguments` and `EnvironmentVariables` configure the test runner, while `UITargetAppCommandLineArguments` and `UITargetAppEnvironmentVariables` configure the application under UI test. Arguments containing spaces stay single arguments.

Every enabled test plan configuration runs, including configurations that repeat the same test targets. XCTest activities are enabled, so tests can use `XCTContext.runActivity` and add `XCTAttachment` objects. The streamed results contain case outcomes and summaries; they do not include attachment downloads or an `.xcresult` bundle.

## Get JSON results

`--json` streams the raw per-case events as NDJSON and ends with an exit record, for piping into your own tooling:

```bash
lim xcode test . --json > results.ndjson
```

Each line is one event:

```json
{"type":"case","testClass":"MyAppTests.LoginTests","method":"testValidLogin","passed":true,"durationMs":12}
{"type":"case","testClass":"MyAppTests.LoginTests","method":"testExpiredToken","passed":false,"durationMs":31,"failureMessage":"XCTAssertEqual failed: ..."}
{"type":"summary","passed":1,"failed":1,"planFinished":true}
{"exitCode":1}
```

A missing `summary` event means the run died partway. `planFinished: false` means the suite started but did not complete its plan.

## Build without running

`--build-only` compiles the test targets on a plain sandbox without acquiring a simulator. The products stay on the sandbox for a later run:

```bash
lim xcode test . --build-only
```

## Run tests from the TypeScript SDK

The TypeScript SDK runs tests through `xcodebuild` with `action: 'build-for-testing'`. Unlike the CLI, it does not create a simulator for you: the tests run only when a simulator is attached to the Xcode sandbox, and without one the call just compiles them. Attach one first, as in [Install on a simulator](/docs/ios/build-with-xcode#install-on-a-simulator). Per-case events stream through `onXctestEvent`, and the accumulated cases and summary land on the result:

```ts
await xcode.attachNewSimulator(); // or xcode.attachSimulator(iosInstance)

const result = await xcode.xcodebuild(
  { scheme: 'MyApp', sdk: 'iphonesimulator', action: 'build-for-testing' },
  { onXctestEvent: (event) => console.log(event) },
);

console.log(result.xctest?.summary); // { type: 'summary', passed, failed, planFinished }
console.log(result.exitCode);        // non-zero when tests fail
```

`onlyTesting` and `skipTesting` go on the same settings object as `scheme`. `runTests: false` compiles the test targets without running them, the SDK equivalent of `--build-only`. `xcode` is the Xcode sandbox client from [Build with Xcode](/docs/ios/build-with-xcode).

## Run in CI

- The exit code is the gate: `0` when every selected test passes, non-zero otherwise, so `lim xcode test .` works as a CI step with no output parsing.
- For reproducible runs on fresh instances, pass `--inactivity-timeout 30m`. The run then creates its own sandbox and simulator instead of reusing yours, and they delete themselves once idle. This flag cannot be combined with `--id`.

[GitHub Actions recipes](/docs/ci/github-actions) shows how to set up the rest of a Linux CI job: install the CLI and pass `LIM_API_KEY` from a secret.

## Next steps

<Columns cols={2}>
  <Card title="Build with Xcode" icon="hammer" href="/docs/ios/build-with-xcode">
    Sync rules, schemes, and build flags shared with `lim xcode test`.
  </Card>
  <Card title="Maestro" icon="flask-conical" href="/docs/testing/maestro">
    Run YAML UI flows against the same cloud simulators.
  </Card>
  <Card title="PR previews" icon="git-pull-request" href="/docs/ci/pr-previews">
    Pair the test run with a preview link on every pull request.
  </Card>
</Columns>