---
title: "XCTestとは？Swift Testingとの使い分けとユニット・UIテストの書き方【Xcode 27対応】"
url: "https://www.issoh.co.jp/tech/details/7318/"
published: 2025-06-19
updated: 2026-10-02
categories: ["テスト"]
publisher: "株式会社一創"
---

# XCTestとは？Swift Testingとの使い分けとユニット・UIテストの書き方【Xcode 27対応】

XCTestは、Xcodeに同梱されているAppleの公式テストフレームワークです。ユニットテスト・UIテスト・パフォーマンステストの3種類を書けます。Xcode 16でSwift Testingが加わってからは、新しく書くユニットテストはSwift Testing、画面操作と性能測定はXCTest、という分担が基本になりました。この記事では、2026年9月14日に公開されたXcode 27を前提に、XCTestの書き方と、Swift Testingとどう併用するかを実行例つきで説明します。

## まとめ：XCTestの役割とSwift Testingとの使い分けの要点

- XCTestは非推奨ではありません。AppleはUIテストとパフォーマンステストにはXCTestを使い続けるよう案内しています。
- 新しいユニットテストはSwift Testingが既定です。Xcode 16以降、Unit Testing Bundleのテンプレートで選ばれる既定のテストシステムはSwift Testingです。
- 1つのテストターゲットに両方を同居させられます。ただし、1つのテスト関数の中で2つのフレームワークのAPIを混ぜないでください。
- 非同期処理は `async throws` のテストメソッドで書き、コールバック型のAPIだけexpectationと `fulfillment(of:timeout:)` で待ちます。
- Xcode 27では相互運用が既定で有効になり、limited・complete・strict・noneの4つのモードから選べます。Xcode 27より前に作ったテストプランはlimited、新規プロジェクトはcompleteで動きます。

以下、テストの種類ごとに書き方を説明し、最後にSwift Testingへ移行するときの判断基準を示します。

## XCTestの位置づけ：Xcode 27でのXCTestとSwift Testingの関係

### XCTestで書ける3種類のテスト

ユニットテストは `XCTestCase` のサブクラスに書き、UIテストは `XCUIApplication` でアプリを別プロセスとして起動して操作し、パフォーマンステストは `measure(metrics:block:)` で処理時間やメモリを記録します。UIテストの部分は「XCUITest」と呼ばれることもあります。Xcode 16.3からは、`XCUIApplication` などのUI操作用APIがXCUIAutomationという独立したフレームワークに移りました。ただしXCTestがXCUIAutomationを再エクスポートしているので、これまでどおり `import XCTest` だけで使えます。

### Swift Testingとの違いと使い分けの基準

Swift TestingはXcode 16で同梱された新しいテストフレームワークで、Swiftのマクロを使って書きます。AppleはXCTestのドキュメントで、Swift TestingとXCTestを同じテストターゲットに共存させてよいとしたうえで、UIテストとパフォーマンステストではXCTestを使い続けるよう案内しています。

| 項目          | XCTest               | Swift Testing    |
| ----------- | -------------------- | ---------------- |
| テストの宣言      | XCTestCase継承＋test接頭辞 | @Test属性          |
| 検証          | XCTAssert系の関数        | #expect／#require |
| 前処理         | setUp系メソッド           | init             |
| パラメータ化      | なし                   | arguments:       |
| UIテスト       | 可（XCUIApplication）   | 不可               |
| 性能測定        | 可（measure）           | 不可               |
| Objective-C | 可                    | 不可               |

新しく書くロジックのテストはSwift Testing、画面操作・性能測定はXCTestにします。Objective-Cの例外を投げるコードも例外で、WWDC26のセッション「Migrate to Swift Testing」によると、SwiftのコードではXCTestで書いても例外を安全に扱えないため、Objective-Cで書いたXCTestでテストする必要があります。

## テストターゲットの追加とテストの実行手順

既存プロジェクトにテストを足すときは、File > New > Target を開き、TestセクションのUnit Testing Bundle（ユニットテスト用）かUI Testing Bundle（UIテスト用）を選びます。Unit Testing Bundleの作成画面には「Testing System」という欄があります。ここでSwift TestingとXCTestのどちらでテンプレートを作るかを選び、Xcode 16以降の既定はSwift Testingです。XCTestで書きたい場合は、ここでXCTestに切り替えます。

⌘U でスキーム内の全テストを、テストナビゲータ（⌘6）や行番号横のひし形アイコンで個別のテストを実行します。テストファイルの先頭に `@testable import アプリのモジュール名` を書くと、`internal` の型やメソッドまでテストから参照できます。

Command Line Toolsだけを入れたMacでは、XCTestを使えません。Command Line Tools（Swift 6.3.3）で `import XCTest` を含むパッケージをビルドすると、`no such module 'XCTest'` で止まりました。Swift TestingのTesting.frameworkは入っていますが、素の `swift test` では見つからず、`-Xswiftc -F` と `-Xlinker -rpath` で `/Library/Developer/CommandLineTools/Library/Developer/Frameworks` と同じ階層の `usr/lib` を指定して初めて実行できました。CIでXCTestを回すならXcode本体が必要です。

## XCTestCaseによるユニットテストの書き方

### XCTestCaseの継承とtest接頭辞のテストメソッド

`XCTestCase` を継承したクラスを作り、名前が `test` で始まる引数なしのメソッドを書くと、XCTestはそれを1件のテストとして実行します。次の例は、在庫を確認してから商品をカートに追加する `Cart` 型をテストするものです。在庫の確認はプロトコル経由で外から差し込めるようにしてあり、テストでは常に決まった答えを返すスタブを渡します。

```
import XCTest
@testable import Cart

final class StubStock: StockChecking {
  let available: Set<String>
  init(_ available: Set<String>) { self.available = available }
  func isAvailable(_ sku: String) async -> Bool { available.contains(sku) }
}

final class CartXCTests: XCTestCase {
  var cart: Cart!

  override func setUpWithError() throws {
    cart = Cart(stock: StubStock(["A", "B"]))
  }

  override func tearDownWithError() throws {
    cart = nil
  }

  func test_add_合計金額が加算される() async throws {
    try await cart.add(Item(sku: "A", price: 300))
    try await cart.add(Item(sku: "B", price: 200))
    XCTAssertEqual(cart.total, 500)
  }

  func test_first_追加した商品を取り出せる() async throws {
    try await cart.add(Item(sku: "A", price: 300))
    let item = try XCTUnwrap(cart.first(sku: "A"))
    XCTAssertEqual(item.price, 300)
  }
}
```

テストメソッド名を「対象\_条件\_期待結果」の形にすると、失敗時にテストナビゲータを見るだけで原因の見当がつきます。依存先をプロトコルで受け取ってスタブを渡す考え方は、[モックとスタブの違いとテストダブル5分類](/tech/details/13505/)で整理しています。

この例は、`Cart` がメインアクターに属さない場合（Swift Packageのライブラリなど）の書き方です。Xcode 26以降で新規作成したアプリは、モジュールの既定がメインアクター分離になっています。その場合、同期の `setUpWithError()` からは `Cart` を作れません。Appleのドキュメントの案内どおり、前処理を `@MainActor override func setUp() async throws` にし、テストメソッドにも `@MainActor` を付けてください。

### setUpWithErrorとtearDownの実行順

テストメソッドを1件実行する前に、XCTestは非同期版の `setUp() async throws`、`setUpWithError()`、`setUp()` の順で前処理を呼びます。テスト後の後処理は `tearDown()`、`tearDownWithError()`、非同期版の順です。`addTeardownBlock` で登録したブロックは、後に登録したものから順に1つずつ実行され、tearDown系メソッドより先に終わります。

新しく書くなら、前処理は `setUpWithError()` にします。ファイルの準備やモックサーバーの起動のように失敗しうる処理を `try` で書け、失敗するとそのテストが失敗として記録されるからです。非同期の準備が要るときだけ `setUp() async throws` を使います。テストごとにインスタンスが作り直されるので、プロパティはテストをまたいで共有されません。`static` なプロパティやシングルトンだけは状態が残るため、後処理で必ず元に戻してください。

### XCTAssert系アサーションの使い分け

| 関数                              | 検証する内容           |
| ------------------------------- | ---------------- |
| XCTAssertEqual                  | 2つの値が等しい         |
| XCTAssertEqual(\_:\_:accuracy:) | 数値の差が許容誤差以内      |
| XCTAssertTrue／False             | 条件式の真偽           |
| XCTAssertNil／NotNil             | Optionalがnilかどうか |
| XCTUnwrap                       | nilでなければ値を返す     |
| XCTAssertThrowsError            | 式がエラーを投げる        |
| XCTFail                         | 無条件に失敗させる        |
| XCTSkipIf／XCTSkipUnless         | 条件次第でテストを飛ばす     |
| XCTExpectFailure                | 既知の失敗として扱う       |

浮動小数点の比較には `accuracy:` 付きの `XCTAssertEqual` を使います。値、期待値、許容誤差の3つは同じ数値型でなければなりません。Optionalを取り出して後続の検証に使うなら、`XCTAssertNotNil` と強制アンラップを組み合わせるより `XCTUnwrap` が安全です。nilのときはクラッシュせず、テストの失敗として記録されます。

`XCTAssertThrowsError` が受け取るのは `@autoclosure () throws` の式なので、`await` が必要な呼び出しは渡せません。async関数がエラーを投げることを確かめるには、`do`–`catch` で受けて型と値を比べます。

```
func test_add_在庫切れはエラーになる() async {
  do {
    try await cart.add(Item(sku: "Z", price: 100))
    XCTFail("outOfStockが投げられるはず")
  } catch {
    XCTAssertEqual(error as? CartError, .outOfStock("Z"))
  }
}
```

## 非同期処理のテスト：async testとXCTestExpectation

### async throwsのテストメソッド

async/awaitを使うコードは、テストメソッドを `async` または `async throws` にすれば、そのまま `await` で呼べます。投げられたエラーはテストの失敗として記録されます。前の章の `test_add_合計金額が加算される` がこの書き方の例で、待ち合わせのためのコードは要りません。

### コールバック型APIを待つexpectationとfulfillment

完了ハンドラで結果を返す古いAPIは、`XCTestExpectation` を作り、コールバックの中で `fulfill()` を呼んで待ちます。asyncのテストメソッドで待つときは `fulfillment(of:timeout:enforceOrder:)` を使ってください。同期版の `wait(for:timeout:)` は、Xcode 14.3からasyncの関数の中では使えない（unavailable）扱いになりました。Appleはテストがデッドロックするおそれがあることを理由に挙げ、代わりに `fulfillment(of:timeout:enforceOrder:)` を使うよう案内しています。

```
func test_loadLegacy_完了ハンドラの値を検証する() async {
  let exp = expectation(description: "completion")
  cart.loadLegacy { value in
    XCTAssertEqual(value, 42)
    exp.fulfill()
  }
  await fulfillment(of: [exp], timeout: 1.0)
}
```

通知が複数回届くことを確かめたいときは、`expectedFulfillmentCount` に必要な回数を入れます。複数のexpectationが決まった順に満たされることを確かめたいなら、`enforceOrder: true` を指定します。ネットワークを実際に呼んで遅いなら、タイムアウトを延ばさず依存先をスタブに差し替えてください。

## XCUITestによるUIテストの書き方

### XCUIApplicationの起動引数と環境変数

UIテストではアプリを別プロセスで起動するため、アプリ内部の変数を直接書き換えられません。テスト用の状態を作るには、`launchArguments`（文字列の配列）と `launchEnvironment`（文字列の辞書）に値を入れてから `launch()` を呼び、アプリ側でそれを読みます。どちらも次の起動のときに渡される値で、起動した後に変えても、動いているアプリには反映されません。

```
import XCTest

final class LoginUITests: XCTestCase {
  override func setUpWithError() throws {
    continueAfterFailure = false
  }

  @MainActor
  func test_ログイン後にホームが表示される() throws {
    let app = XCUIApplication()
    app.launchArguments = ["-uitest"]
    app.launchEnvironment["API_BASE_URL"] = "http://localhost:8080"
    app.launch()

    let email = app.textFields["login.email"]
    email.tap()
    email.typeText("demo@example.com")
    app.secureTextFields["login.password"].tap()
    app.secureTextFields["login.password"].typeText("password")
    app.buttons["login.submit"].tap()

    XCTAssertTrue(app.staticTexts["home.title"].waitForExistence(timeout: 5))
  }
}
```

### accessibilityIdentifierとwaitForExistenceによる要素の特定

上の例の `"login.email"` は、アプリ側のSwiftUIで `.accessibilityIdentifier("login.email")` と付けた識別子です。この値はユーザーには表示されないため、表示文言を翻訳・変更してもテストは壊れません。UIテストを書き始める前に、識別子の命名ルールをチームで決めておいてください。

画面遷移や通信の結果を待つときは、`sleep` で固定時間止めるのではなく `waitForExistence(timeout:)` を使います。要素が現れた時点で次に進み、タイムアウトしたら `false` を返すので、`XCTAssertTrue` で包めば失敗の場所がはっきりします。たまに落ちる不安定なUIテストの原因は、待ち方が固定時間になっていることと、前のテストが残したログイン状態などに引きずられていることがよくあります。`launchArguments` でテストごとに初期状態を作り直せば、両方とも防げます。

### UI automation recordingによる操作の記録

Xcode 26でUIテストの記録機能が新しくなりました。テストメソッドの中で、エディタの端にある記録ボタンを押すと、シミュレータや実機での操作がテストコードとして書き込まれます。生成されたコードは要素を表示文言で探していることがあります。記録された要素クエリには同じ要素を指す別の候補が用意されているので、識別子で探すものに切り替えると、文言の変更に強くなります。実機やクラウドの多数の端末で回したい場合は、[Firebase Test Labの実機と仮想端末の使い分け](/tech/details/17046/)や、XCUITestを内部で使う[Appium 3でのモバイルテスト自動化](/tech/details/10729/)も比べてみてください。

## パフォーマンステストのmeasureとベースライン

`measure(metrics:block:)` にメトリクスを渡すと、ブロックを繰り返し実行して値を記録します。`XCTClockMetric` は経過時間で、CPUが待機していた時間も含みます。`XCTMemoryMetric` は、ブロックを実行する前と後の物理メモリ使用量の差を記録します。

```
func test_並べ替えの処理時間とメモリ() {
  let input = (0..<100_000).map { _ in Int.random(in: 0..<1_000_000) }
  measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
    _ = input.sorted()
  }
}
```

記録した結果からベースラインと許容する最大標準偏差を設定すると、測った値がその範囲を超えて悪化したときにテストが失敗します。ベースラインが無いあいだは値を記録するだけで、比較はされません。Appleのドキュメントは、性能テストを実際の動作に近い条件で測るため、テスト用のビルドをRelease構成にして「Debug executable」を切り、そのテストプランではコードカバレッジとランタイムサニタイザを無効にするよう勧めています。ユニットテストと同じテストプランのまま測ると、カバレッジ収集の負荷まで数値に入ります。

## xcodebuildでのCI実行とカバレッジ取得

CIではXcodeの画面を使わず、`xcodebuild test` でテストを実行します。`-only-testing` と `-skip-testing` には「ターゲット/クラス/メソッド」の形で、実行するテストと除外するテストを指定できます。`-resultBundlePath` を付けると、結果が `.xcresult` バンドルとして保存されます。

```
xcodebuild test \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 17' \
  -only-testing:MyAppTests/CartXCTests \
  -enableCodeCoverage YES \
  -resultBundlePath build/TestResults.xcresult

xcrun xccov view --report build/TestResults.xcresult
xcrun xcresulttool get test-results summary --path build/TestResults.xcresult
```

`-destination` に書くシミュレータ名は、`xcrun simctl list devices` で確認して、ランナーに入っているものに合わせてください。`xccov view --report` はファイルごとのカバレッジ率を表示します。目標値の決め方は[テストカバレッジの網羅率と目標設定](/tech/details/13507/)で説明しています。実行するテストや環境変数の組み合わせを切り替えたいときは、テストプラン（`.xctestplan`）を1つのスキームに複数持たせます。ビルドから配信までの自動化には、[fastlaneでのiOSビルドと配信の自動化](/tech/details/5686/)も使えます。

## Swift Testingへの移行判断と相互運用モード

### 同じ処理をSwift Testingで書いた例

前の章の在庫チェックのテストをSwift Testingで書き直すと、次のようになります。`StubStock` は前の章のXCTestのファイルで定義したものを、同じテストターゲットからそのまま使います。これは筆者の環境のCommand Line Tools（Swift 6.3.3）で、フレームワークの検索パスを指定して `swift test` を実行し、パラメータ化した2ケースを含む2件のテストが通ることを確認したコードです。テストごとに構造体のインスタンスが新しく作られるので、`setUp` にあたる処理はプロパティの初期化に書きます。

```
import Testing
@testable import Cart

struct CartSwiftTests {
  let cart = Cart(stock: StubStock(["A", "B"]))

  @Test("在庫がある商品は追加できる", arguments: ["A", "B"])
  func addAvailable(sku: String) async throws {
    try await cart.add(Item(sku: sku, price: 100))
    #expect(cart.total == 100)
  }

  @Test func outOfStock() async {
    await #expect(throws: CartError.outOfStock("Z")) {
      try await cart.add(Item(sku: "Z", price: 100))
    }
  }
}
```

XCTestでは `do`–`catch` が必要だったasync関数のエラー検証が、`#expect(throws:)` の1回で書けます。値だけが違う似たテストは `arguments:` で1本にまとめられ、結果には引数ごとの成否が表示されます。

テスト対象がメインアクター既定のアプリのモジュールなら、スイートに `@MainActor` を付けて `@MainActor struct CartSwiftTests` とします。付けないと、`main actor-isolated property 'total' cannot be accessed from outside of the actor` などのエラーでビルドできません。逆に、メインアクターに属さない型のテストに一律で付けると、別のデータ競合のエラーになることがあります。

### Xcode 27の4つの相互運用モード

Swift TestingのテストからXCTestのAPI（たとえば `XCTFail` を中で呼ぶ共通ヘルパー）を呼ぶと、フレームワークをまたいだ失敗になります。WWDC26のセッション「Migrate to Swift Testing」によると、Xcodeはこの相互運用を既定で有効にしています。扱い方は次の4つのモードから選びます。

| モード      | XCTest由来の失敗の扱い  | 既定になる場合               |
| -------- | --------------- | --------------------- |
| limited  | 警告              | Xcode 27より前に作ったテストプラン |
| complete | エラーのまま          | 新規プロジェクト              |
| strict   | 致命的エラーでテストを止める  | なし                    |
| none     | 報告しない（相互運用を無効化） | なし                    |

モードは、テストプランの設定画面のTest Executionで変更します。Swift Packageでは、Swift 6.4のツールチェーンで `swift-tools-version` が6.4未満（例：6.3）のパッケージを実行するとlimited、6.4以上に上げるとcompleteになります。環境変数 `SWIFT_TESTING_XCTEST_INTEROP_MODE` にモード名を小文字で入れて、どちらも上書きできます。

注意が要るのは、既存プロジェクトをXcode 27で開いたときです。limitedでは、Swift Testingのテストの中で起きた `XCTFail` が警告に格下げされるので、本当は失敗しているテストがCIで緑のまま通ります。移行途中のプロジェクトはcompleteに上げ、XCTestのAPIを全部置き換えられたらstrictにして、XCTestのAPIが残っていないことを確かめてください。noneは、フレームワークをまたいだ失敗を両方向とも報告しなくなります。WWDC26のセッションも、使うなら一時的な回避に限り、completeかstrictを選ぶよう勧めています。

### XCTestを残すべきテストと移行の順番

すべてを一度にSwift Testingへ書き直す必要はありません。WWDC26のセッションが勧めているのも段階的なやり方です。既存のXCTestはそのまま残し、新しいテストはすぐSwift Testingで書き始め、既存のテストはよく更新するものから少しずつ移していきます。UIテスト・`measure` を使う性能テスト・Objective-Cのテストは移せないので、XCTestのまま残します。よく更新するテストの中でも、入力値だけが違うテストメソッドの集まりは、パラメータ化で本数を大きく減らせるので先に移す価値があります。単体・結合・E2Eのテストをどの割合で書くかは、[テストピラミッドの配分とテスト戦略設計](/tech/details/13289/)を参考にしてください。

## よくある質問

### XCTestは非推奨になったのですか？

なっていません。廃止されたのは一部のシンボルだけで、フレームワーク自体はXcode 27にも同梱されています。変わったのは、新しく書くユニットテストの既定がSwift Testingになったことです。

### XCTestとXCUITestは何が違うのですか？

XCUITestは、UIテスト用のAPI群（`XCUIApplication` や `XCUIElement` など）の通称です。Xcode 16.3からこれらのAPIはXCUIAutomationという独立したフレームワークに移りましたが、XCTestが再エクスポートしているため、テストコードは `import XCTest` のままで使えます。UIテストはUI Testing Bundleのターゲットに書きます。

### Xcodeの「Testing System」とは何ですか？

Unit Testing Bundleのテンプレートを、Swift TestingとXCTestのどちらで作るかを選ぶ欄です。既定はSwift Testingで、後から同じターゲットにもう一方のテストを足すこともできます。

### setUpとsetUpWithErrorはどちらを使えばよいですか？

setUpWithErrorを使ってください。準備の失敗をエラーとして記録できます。両方を実装するとsetUpWithErrorが先に呼ばれます。

### XCTestExpectationはasync/awaitでも必要ですか？

asyncのAPIは `await` で呼ぶだけなので不要です。完了ハンドラや通知で結果を返すAPIを待つときだけ使い、`fulfillment(of:timeout:)` で待ちます。

## 関連記事

- [モックとは？スタブとの違い・テストダブル5分類と単体テストでの使い分けを実装者向けに解説](/tech/details/13505/)
- [テストピラミッドとは？単体・結合・E2Eの配分と実装者向けテスト戦略設計](/tech/details/13289/)
- [テストカバレッジとは？C0/C1/C2の網羅率と計測ツール・目標設定を実装者向けに解説](/tech/details/13507/)
- [Firebase Test Labとは｜実機と仮想端末の課金単位とCI組み込みの設計【2026年8月時点】](/tech/details/17046/)
- [fastlaneとは？iOS・Androidのビルドと配信を自動化する使い方【2.240対応】](/tech/details/5686/)

---

出典: [XCTestとは？Swift Testingとの使い分けとユニット・UIテストの書き方【Xcode 27対応】](<https://www.issoh.co.jp/tech/details/7318/>)（株式会社一創）
