ガイド — テスト

Macサーバーでの自動UIテスト: XCTest、Appium、Selenium

24時間365日の自動UIテストのために専用のMacサーバーをセットアップしましょう。複数のiOS Simulatorにまたがって、XCTest、XCUITest、Appium、Seleniumのテストを並列に実行し、一貫した再現性のある結果を得られます。

読了目安30分 2026年3月更新

なぜテストに専用ハードウェアなのか?

共有マシンやローカルマシンでのUIテストは、不安定さ(flakiness)、一貫性の欠如、ボトルネックを招きます。専用のMacサーバーはこうした問題を解決します。

一貫した結果

CPUやRAMを奪い合う他のプロセスがありません。テストは毎回クリーンで制御された環境で実行され、リソースの競合に起因する不安定なテストの失敗をなくします。

並列実行

複数のiOS Simulatorインスタンスにまたがってテストを同時に実行します。M4 Proの14コアは、性能を損なうことなく4〜6個の並列Simulatorインスタンスを処理します。

24時間365日の可用性

テストはいつでも実行できます——夜間のリグレッションスイート、マージ後の検証、オンデマンドのいずれも。サーバーは常に準備万端で、開発者のノートパソコンは不要です。

専用MacでのXCTest

XCTestは、Xcodeに含まれるApple標準のテストフレームワークです。XCUITestはこれをUIテスト向けに拡張したものです。どちらもネイティブに動作し、Xcode以外の追加セットアップは不要です。

コマンドラインからXCTestを実行する

# Run all unit tests
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
  -resultBundlePath ./TestResults/UnitTests.xcresult

# Run only UI tests
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyAppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
  -resultBundlePath ./TestResults/UITests.xcresult

# Run a specific test class
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:MyAppTests/LoginTests

# Run a specific test method
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -only-testing:MyAppTests/LoginTests/testSuccessfulLogin

複数のデスティネーションでテストする

# Test on multiple iPhone models simultaneously
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro Max' \
  -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
  -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)' \
  -resultBundlePath ./TestResults/MultiDevice.xcresult

# List all available destinations
xcodebuild -showdestinations \
  -workspace MyApp.xcworkspace \
  -scheme MyApp

結果バンドルとスクリーンショット

# Extract test results summary
xcrun xcresulttool get --path ./TestResults/UITests.xcresult \
  --format json

# Export test attachments (screenshots, videos)
xcrun xcresulttool export \
  --path ./TestResults/UITests.xcresult \
  --output-path ./TestArtifacts \
  --type attachments

# Get human-readable test summary
xcrun xcresulttool get --path ./TestResults/UITests.xcresult \
  --format json | python3 -m json.tool

iOS向けAppiumのセットアップ

Appiumはオープンソースの自動化フレームワークで、任意の言語(Python、JavaScript、Javaなど)でテストを書き、シミュレータや実機上のiOSアプリに対して実行できます。内部ではAppleのXCUITestドライバーを使用しています。

MacサーバーにAppiumをインストールする

# Install Node.js via Homebrew
brew install node

# Install Appium 2.x globally
npm install -g appium

# Install the XCUITest driver for iOS
appium driver install xcuitest

# Verify installation
appium --version
appium driver list --installed

# Install appium-doctor to check dependencies
npm install -g appium-doctor
appium-doctor --ios

# Start Appium server
appium server --address 127.0.0.1 --port 4723

Desired Capabilitiesを構成する

# Example capabilities (JSON format for Appium 2.x)
{
    "platformName": "iOS",
    "appium:automationName": "XCUITest",
    "appium:deviceName": "iPhone 16",
    "appium:platformVersion": "18.2",
    "appium:app": "/path/to/MyApp.app",
    "appium:noReset": false,
    "appium:wdaStartupRetries": 3,
    "appium:wdaStartupRetryInterval": 20000,
    "appium:simulatorStartupTimeout": 120000
}

Appiumテストの例(Python)

# Install Appium Python client
# pip install Appium-Python-Client

from appium import webdriver
from appium.options.ios import XCUITestOptions
from appium.webdriver.common.appiumby import AppiumBy

# Configure options
options = XCUITestOptions()
options.platform_name = "iOS"
options.device_name = "iPhone 16"
options.platform_version = "18.2"
options.app = "/path/to/MyApp.app"

# Connect to Appium server
driver = webdriver.Remote(
    command_executor="http://127.0.0.1:4723",
    options=options
)

try:
    # Wait for app to load
    driver.implicitly_wait(10)

    # Find and tap login button
    login_button = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "loginButton"
    )
    login_button.click()

    # Enter username
    username_field = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "usernameField"
    )
    username_field.send_keys("testuser@example.com")

    # Enter password
    password_field = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "passwordField"
    )
    password_field.send_keys("password123")

    # Submit login
    submit_button = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "submitButton"
    )
    submit_button.click()

    # Verify welcome screen
    welcome_label = driver.find_element(
        AppiumBy.ACCESSIBILITY_ID, "welcomeLabel"
    )
    assert "Welcome" in welcome_label.text

    print("Test PASSED: Login successful")

finally:
    driver.quit()

Appiumテストの例(JavaScript)

// npm install webdriverio @wdio/cli

const { remote } = require('webdriverio');

async function runTest() {
    const driver = await remote({
        protocol: 'http',
        hostname: '127.0.0.1',
        port: 4723,
        path: '/',
        capabilities: {
            platformName: 'iOS',
            'appium:automationName': 'XCUITest',
            'appium:deviceName': 'iPhone 16',
            'appium:platformVersion': '18.2',
            'appium:app': '/path/to/MyApp.app'
        }
    });

    try {
        // Tap login button
        const loginBtn = await driver.$('~loginButton');
        await loginBtn.click();

        // Enter credentials
        const username = await driver.$('~usernameField');
        await username.setValue('testuser@example.com');

        const password = await driver.$('~passwordField');
        await password.setValue('password123');

        // Submit
        const submit = await driver.$('~submitButton');
        await submit.click();

        // Verify
        const welcome = await driver.$('~welcomeLabel');
        const text = await welcome.getText();
        console.assert(text.includes('Welcome'), 'Login test failed');

        console.log('Test PASSED: Login successful');
    } finally {
        await driver.deleteSession();
    }
}

runTest();

Safari向けSelenium

macOSには、Safariとsafaridriverが最初から含まれています。他のプラットフォームのChromeやFirefoxとは異なり、追加のブラウザドライバーのダウンロードは不要です。

safaridriverを有効にする

# Enable the Safari WebDriver (one-time setup)
safaridriver --enable

# Verify safaridriver is working
safaridriver --version

# For headless-like automation, enable "Allow Remote Automation"
# in Safari > Settings > Advanced > Show Develop menu
# Then: Develop > Allow Remote Automation
# Or via command line:
defaults write com.apple.Safari AllowRemoteAutomation 1

Selenium Safariテスト(Python)

# pip install selenium

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

# Create Safari driver
driver = webdriver.Safari()

try:
    # Navigate to your web app
    driver.get("https://your-webapp.com")

    # Wait for page to load
    wait = WebDriverWait(driver, 10)

    # Find and click a button
    login_link = wait.until(
        EC.element_to_be_clickable((By.CSS_SELECTOR, "a.login-btn"))
    )
    login_link.click()

    # Fill in form
    email_input = wait.until(
        EC.presence_of_element_located((By.ID, "email"))
    )
    email_input.send_keys("test@example.com")

    password_input = driver.find_element(By.ID, "password")
    password_input.send_keys("testpassword")

    # Submit form
    submit_btn = driver.find_element(By.CSS_SELECTOR, "button[type='submit']")
    submit_btn.click()

    # Verify redirect to dashboard
    wait.until(EC.url_contains("/dashboard"))
    assert "/dashboard" in driver.current_url

    print("Test PASSED: Safari login flow works correctly")

finally:
    driver.quit()

Selenium Safariテスト(JavaScript)

// npm install selenium-webdriver

const { Builder, By, until } = require('selenium-webdriver');

async function safariTest() {
    const driver = await new Builder()
        .forBrowser('safari')
        .build();

    try {
        await driver.get('https://your-webapp.com');

        // Click login
        const loginBtn = await driver.findElement(By.css('a.login-btn'));
        await loginBtn.click();

        // Fill form
        const email = await driver.findElement(By.id('email'));
        await email.sendKeys('test@example.com');

        const password = await driver.findElement(By.id('password'));
        await password.sendKeys('testpassword');

        // Submit
        const submit = await driver.findElement(
            By.css("button[type='submit']")
        );
        await submit.click();

        // Verify
        await driver.wait(until.urlContains('/dashboard'), 10000);
        const url = await driver.getCurrentUrl();
        console.assert(url.includes('/dashboard'));

        console.log('Test PASSED: Safari login flow works');
    } finally {
        await driver.quit();
    }
}

safariTest();

並列テスト実行

テストを並列に実行すると、テストスイート全体の実行時間を劇的に短縮できます。Mac Mini M4 Proは、4〜6個のSimulatorインスタンスを余裕をもって同時に実行できます。

XCTestの並列テスト

# Enable parallel testing with xcodebuild
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -parallel-testing-enabled YES \
  -parallel-testing-worker-count 4 \
  -resultBundlePath ./TestResults/Parallel.xcresult

# Parallel testing across multiple device types
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyApp \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
  -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)' \
  -parallel-testing-enabled YES \
  -resultBundlePath ./TestResults/MultiDeviceParallel.xcresult

Simulatorインスタンスの管理

# List all available simulators
xcrun simctl list devices available

# Create custom simulator instances for testing
xcrun simctl create "Test-iPhone-1" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-2" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-3" "iPhone 16" "iOS 18.2"
xcrun simctl create "Test-iPhone-4" "iPhone 16" "iOS 18.2"

# Boot multiple simulators
xcrun simctl boot "Test-iPhone-1"
xcrun simctl boot "Test-iPhone-2"
xcrun simctl boot "Test-iPhone-3"
xcrun simctl boot "Test-iPhone-4"

# Check booted simulators
xcrun simctl list devices booted

# Shut down all simulators
xcrun simctl shutdown all

# Delete all test simulators
xcrun simctl delete "Test-iPhone-1"
xcrun simctl delete "Test-iPhone-2"
xcrun simctl delete "Test-iPhone-3"
xcrun simctl delete "Test-iPhone-4"

並列テストの性能

構成 UIテスト200件の所要時間 速度向上
1 Simulator (sequential) 45 minutes Baseline
2 Simulators (parallel) 24 minutes 1.9x faster
4 Simulators (parallel) 13 minutes 3.5x faster
6 Simulators (parallel) 10 minutes 4.5x faster

注: 結果はMac Mini M4 Pro(14コア、24GB RAM)で測定しました。並列Simulatorの最適な数は、テストの複雑さとメモリ要件によって異なります。ほとんどのUIテストスイートでは、4つの並列ワーカーが速度と安定性の最良のバランスをもたらします。

CI/CD連携

自動テストをCI/CDパイプラインに組み込み、プッシュ、プルリクエスト、あるいはスケジュールされた間隔ごとに自動でテストを実行しましょう。

自動テストのためのGitHub Actionsワークフロー

# .github/workflows/ios-tests.yml
name: iOS Automated Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    # Nightly regression tests at 2 AM UTC
    - cron: '0 2 * * *'

jobs:
  unit-tests:
    name: Unit Tests
    runs-on: self-hosted
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app

      - name: Resolve dependencies
        run: |
          xcodebuild -resolvePackageDependencies \
            -workspace MyApp.xcworkspace \
            -scheme MyApp

      - name: Run unit tests
        run: |
          xcodebuild test \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.2' \
            -parallel-testing-enabled YES \
            -resultBundlePath $/UnitTests.xcresult

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: unit-test-results
          path: $/UnitTests.xcresult

  ui-tests:
    name: UI Tests
    runs-on: self-hosted
    timeout-minutes: 60
    needs: unit-tests

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode version
        run: sudo xcode-select -s /Applications/Xcode_16.2.app

      - name: Boot simulators for parallel testing
        run: |
          xcrun simctl boot "iPhone 16" || true
          xcrun simctl boot "iPhone SE (3rd generation)" || true

      - name: Run UI tests in parallel
        run: |
          xcodebuild test \
            -workspace MyApp.xcworkspace \
            -scheme MyAppUITests \
            -destination 'platform=iOS Simulator,name=iPhone 16' \
            -destination 'platform=iOS Simulator,name=iPhone SE (3rd generation)' \
            -parallel-testing-enabled YES \
            -parallel-testing-worker-count 4 \
            -resultBundlePath $/UITests.xcresult

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: ui-test-results
          path: $/UITests.xcresult

      - name: Cleanup simulators
        if: always()
        run: xcrun simctl shutdown all

CIでAppiumテストを実行する

# Add to your GitHub Actions workflow
      - name: Start Appium server
        run: |
          appium server --address 127.0.0.1 --port 4723 &
          sleep 5
          curl http://127.0.0.1:4723/status

      - name: Build app for testing
        run: |
          xcodebuild build-for-testing \
            -workspace MyApp.xcworkspace \
            -scheme MyApp \
            -destination 'platform=iOS Simulator,name=iPhone 16'

      - name: Run Appium tests
        run: |
          cd tests/appium
          pip install -r requirements.txt
          pytest test_login.py test_checkout.py \
            --junitxml=results.xml -v

      - name: Stop Appium server
        if: always()
        run: pkill -f appium || true

テストレポート

テストレポートを生成・処理して、品質の傾向を追跡し、失敗しているテストを素早く特定しましょう。

xcresultバンドルを扱う

# Get test results summary as JSON
xcrun xcresulttool get \
  --path ./TestResults.xcresult \
  --format json

# Get specific test action results
xcrun xcresulttool get \
  --path ./TestResults.xcresult \
  --format json \
  --id "REF_ID"

# Export all attachments (screenshots, logs)
xcrun xcresulttool export \
  --path ./TestResults.xcresult \
  --output-path ./artifacts \
  --type attachments

# Merge multiple xcresult bundles
xcrun xcresulttool merge \
  ./UnitTests.xcresult \
  ./UITests.xcresult \
  --output-path ./MergedResults.xcresult

JUnit XMLに変換する

JUnit XMLは、CI/CDプラットフォーム、Slack連携、テストダッシュボードでサポートされる汎用フォーマットです。

# Install xcresult-to-junit converter
brew install chargepoint/xcparse/xcparse

# Convert xcresult to JUnit XML
xcparse tests ./TestResults.xcresult ./junit-results/

# Or use trainer (a popular Ruby gem)
gem install trainer
trainer --path ./TestResults.xcresult --output_directory ./reports

# The generated JUnit XML works with:
# - GitHub Actions test summaries
# - Jenkins Test Result plugin
# - GitLab CI test reporting
# - Slack notifications via CI integrations

失敗時のスクリーンショット

// In your XCUITest, add screenshot capture on failure:
// XCTestCase+Screenshots.swift

import XCTest

extension XCTestCase {
    override func tearDown() {
        if testRun?.hasSucceeded == false {
            let screenshot = XCUIScreen.main.screenshot()
            let attachment = XCTAttachment(screenshot: screenshot)
            attachment.name = "Failure-\(name)"
            attachment.lifetime = .keepAlways
            add(attachment)
        }
        super.tearDown()
    }
}

ベストプラクティス

テストの分離

各テストは独立しているべきであり、前のテストの状態に依存すべきではありません。各テストの前にアプリの状態をリセットしましょう。

// In your XCUITest setUp() method:
override func setUp() {
    super.setUp()
    continueAfterFailure = false

    let app = XCUIApplication()
    app.launchArguments = ["--uitesting", "--reset-state"]
    app.launchEnvironment = [
        "DISABLE_ANIMATIONS": "1",
        "UI_TEST_MODE": "true"
    ]
    app.launch()
}

Simulatorの管理

状態のリークやディスク容量の問題を防ぐため、テスト実行の合間にシミュレータをクリーンアップしましょう。

#!/bin/bash
# cleanup-simulators.sh - Run before and after test suites

# Shutdown all running simulators
xcrun simctl shutdown all

# Erase all simulator content and settings
xcrun simctl erase all

# Delete unavailable simulators
xcrun simctl delete unavailable

# Clear DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# Clear simulator logs
rm -rf ~/Library/Logs/CoreSimulator/*

echo "Simulator cleanup complete"

速度のためにアニメーションを無効化する

UIテスト中はアプリのアニメーションを無効にして、テストの実行時間と不安定さを減らしましょう。

// In your AppDelegate or App struct:
#if DEBUG
if CommandLine.arguments.contains("--uitesting") {
    UIView.setAnimationsEnabled(false)
}
#endif

// Also disable Simulator animations via command line:
// Set Simulator > Debug > Slow Animations = OFF
defaults write com.apple.iphonesimulator SlowMotionAnimation -bool NO

不安定なテストを再試行する

Xcodeに組み込まれたテスト再試行の仕組みを使って、ときおり不安定になるUIテストに対処しましょう。

# Retry failed tests up to 3 times
xcodebuild test \
  -workspace MyApp.xcworkspace \
  -scheme MyAppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16' \
  -retry-tests-on-failure \
  -test-iterations 3 \
  -resultBundlePath ./TestResults.xcresult

ディスク容量を監視する

シミュレータやテスト成果物は多くのディスク容量を消費します。自動クリーンアップをセットアップしましょう。

# Add to crontab for daily cleanup at midnight
# crontab -e
0 0 * * * /usr/local/bin/cleanup-test-artifacts.sh

# cleanup-test-artifacts.sh
#!/bin/bash
# Remove test results older than 7 days
find ~/TestResults -name "*.xcresult" -mtime +7 -delete

# Remove DerivedData older than 3 days
find ~/Library/Developer/Xcode/DerivedData \
  -maxdepth 1 -mtime +3 -exec rm -rf {} +

# Remove old simulator logs
find ~/Library/Logs/CoreSimulator -mtime +3 -delete

# Check remaining disk space
df -h / | tail -1

よくある質問

Mac Mini M4 Proは並列シミュレータをいくつ処理できますか?

14個のCPUコアと24GBのRAMを備えるM4 Proは、UIテスト向けに4〜6個の並列iOS Simulatorインスタンスを余裕をもって処理します。ユニットテストのみ(SimulatorのGUIなし)であれば、さらに多くの並列ワーカーを実行できます。まずは4から始め、テストスイートのメモリ要件に応じて増やすことをおすすめします。

VNCなしでテストをヘッドレスに実行できますか?

はい。XCTestおよびAppiumのテストは、SSH経由のコマンドラインだけで完全に実行できます。iOS Simulatorは、ディスプレイセッションなしでxcodebuildから起動されると"ヘッドレス"モードで動作します。テスト実行中にVNCを接続する必要はありません。VNCは、失敗したテストを視覚的にデバッグするときにのみ役立ちます。

不安定なUIテストにはどう対処すればよいですか?

まず、テストの分離を徹底します(各テストの前にアプリの状態をリセット)。アニメーションを無効にします。sleep()ではなく明示的な待機(explicit wait)を使います。Xcodeの-retry-tests-on-failureフラグを使って、失敗したテストを自動的に再試行します。専用サーバーでは、共有CIマシンで不安定なテストの最も一般的な原因である、リソースの競合による不安定さがなくなります。

リモートのMacから実際のiOSデバイスでテストできますか?

物理的なiOSデバイスをリモートサーバーに接続することはできません(USBはローカルです)。とはいえ、iOS SimulatorはUIテストのシナリオの大半をカバーします。デバイス固有のテストには、Appleの"Xcode Cloud"のデバイスファームを使うか、TestFlight経由でテストビルドを配信して手動でのデバイステストを行えます。

異なるiOSバージョンでのテストはどうですか?

同じMacに複数のiOS Simulatorランタイムをインストールできます。最新版にはxcodebuild -downloadPlatform iOSを使うか、Xcodeの設定 > Platformsから古いランタイムをダウンロードします。そのうえで、テストのデスティネーションでOSバージョンを指定します: -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'

テストスイートを24時間365日実行しましょう

自動テストのために専用のMac Mini M4 Proを手に入れましょう。並列実行、一貫した結果、そして常時稼働の可用性を実現します。

関連ガイド

さらに詳しい情報をお探しですか?

ステップバイステップのセットアップ手順、設定リファレンス、トラブルシューティングについては、完全なドキュメントをご覧ください。

ドキュメントを開く →