Mengapa Hardware Khusus untuk Pengujian?
Pengujian UI pada mesin bersama atau lokal menimbulkan ketidakstabilan, inkonsistensi, dan bottleneck. Server Mac khusus mengatasi masalah-masalah ini.
Hasil yang Konsisten
Tidak ada proses lain yang bersaing memperebutkan CPU atau RAM. Pengujian berjalan dalam lingkungan yang bersih dan terkontrol setiap saat, menghilangkan kegagalan pengujian yang tidak stabil akibat perebutan sumber daya.
Eksekusi Paralel
Jalankan pengujian di beberapa instance iOS Simulator secara bersamaan. 14 core milik M4 Pro menangani 4-6 instance Simulator paralel tanpa penurunan performa.
Ketersediaan 24/7
Pengujian berjalan kapan saja -- suite regresi malam hari, validasi pasca-merge, atau sesuai permintaan. Server selalu siap, tanpa memerlukan laptop developer.
XCTest pada Mac Khusus
XCTest adalah framework pengujian bawaan Apple, yang disertakan dengan Xcode. XCUITest memperluasnya untuk pengujian UI. Keduanya berjalan secara native dan tidak memerlukan setup tambahan selain Xcode.
Menjalankan XCTest dari Command Line
# 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
Pengujian pada Beberapa Destinasi
# 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
Result Bundle dan Screenshot
# 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
Setup Appium untuk iOS
Appium adalah framework otomasi open-source yang memungkinkan Anda menulis pengujian dalam bahasa apa pun (Python, JavaScript, Java, dll.) dan menjalankannya terhadap aplikasi iOS pada simulator atau perangkat. Ia menggunakan driver XCUITest milik Apple di baliknya.
Instal Appium pada Server Mac Anda
# 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
Mengonfigurasi 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
}
Contoh Pengujian 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()
Contoh Pengujian 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();
Selenium untuk Safari
macOS menyertakan Safari dan safaridriver secara bawaan. Tidak perlu mengunduh driver browser tambahan -- tidak seperti Chrome atau Firefox pada platform lain.
Mengaktifkan 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
Pengujian Safari Selenium (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()
Pengujian Safari Selenium (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();
Eksekusi Pengujian Paralel
Menjalankan pengujian secara paralel secara dramatis mengurangi total waktu eksekusi suite pengujian Anda. Mac Mini M4 Pro dapat dengan nyaman menjalankan 4-6 instance Simulator secara bersamaan.
Pengujian Paralel 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
Mengelola Instance 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"
Performa Pengujian Paralel
| Konfigurasi | Durasi 200 Pengujian UI | Peningkatan Kecepatan |
|---|---|---|
| 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 |
Catatan: Hasil diukur pada Mac Mini M4 Pro (14-core, RAM 24GB). Jumlah Simulator paralel yang optimal bergantung pada kompleksitas pengujian dan kebutuhan memori Anda. Untuk sebagian besar suite pengujian UI, 4 worker paralel memberikan keseimbangan terbaik antara kecepatan dan stabilitas.
Integrasi CI/CD
Integrasikan pengujian otomatis Anda ke dalam pipeline CI/CD untuk eksekusi pengujian otomatis pada setiap push, pull request, atau interval terjadwal.
Workflow GitHub Actions untuk Pengujian Otomatis
# .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
Menjalankan Pengujian Appium di CI
# 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
Pelaporan Pengujian
Hasilkan dan proses laporan pengujian untuk melacak tren kualitas dan dengan cepat mengidentifikasi pengujian yang gagal.
Bekerja dengan Bundle 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
Mengonversi ke JUnit XML
JUnit XML adalah format universal yang didukung oleh platform CI/CD, integrasi Slack, dan dashboard pengujian.
# 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
Screenshot saat Kegagalan
// 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()
}
}
Praktik Terbaik
Isolasi Pengujian
Setiap pengujian harus independen dan tidak bergantung pada state dari pengujian sebelumnya. Reset state aplikasi sebelum setiap pengujian.
// 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()
}
Manajemen Simulator
Bersihkan simulator di antara proses pengujian untuk mencegah kebocoran state dan masalah ruang disk.
#!/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"
Nonaktifkan Animasi untuk Kecepatan
Nonaktifkan animasi dalam aplikasi Anda selama pengujian UI untuk mengurangi waktu eksekusi pengujian dan ketidakstabilan.
// 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
Coba Ulang Pengujian yang Tidak Stabil
Gunakan mekanisme coba ulang pengujian bawaan Xcode untuk menangani pengujian UI yang sesekali tidak stabil.
# 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
Pantau Ruang Disk
Simulator dan artefak pengujian mengonsumsi ruang disk yang signifikan. Siapkan pembersihan otomatis.
# 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
Pertanyaan yang Sering Diajukan
Berapa banyak simulator paralel yang dapat ditangani Mac Mini M4 Pro?
Dengan 14 core CPU dan RAM 24GB, M4 Pro dengan nyaman menangani 4-6 instance iOS Simulator paralel untuk pengujian UI. Untuk pengujian unit saja (tanpa GUI Simulator), Anda dapat menjalankan lebih banyak worker paralel. Kami merekomendasikan memulai dengan 4 dan menambahnya berdasarkan kebutuhan memori suite pengujian Anda.
Bisakah saya menjalankan pengujian secara headless tanpa VNC?
Ya. Pengujian XCTest dan Appium berjalan sepenuhnya dari command line via SSH. iOS Simulator berjalan dalam mode "headless" ketika diluncurkan via xcodebuild tanpa sesi tampilan. Anda tidak perlu VNC terhubung selama eksekusi pengujian. VNC hanya berguna untuk men-debug pengujian yang gagal secara visual.
Bagaimana cara menangani pengujian UI yang tidak stabil?
Pertama, pastikan isolasi pengujian (reset state aplikasi sebelum setiap pengujian). Nonaktifkan animasi. Gunakan explicit wait alih-alih sleep(). Gunakan flag -retry-tests-on-failure milik Xcode untuk secara otomatis mencoba ulang pengujian yang gagal. Pada server khusus, ketidakstabilan akibat perebutan sumber daya dihilangkan, yang merupakan penyebab paling umum pengujian tidak stabil pada mesin CI bersama.
Bisakah saya menguji pada perangkat iOS asli dari Mac jarak jauh?
Anda tidak dapat menghubungkan perangkat iOS fisik ke server jarak jauh (USB bersifat lokal). Namun, iOS Simulator mencakup sebagian besar skenario pengujian UI. Untuk pengujian khusus perangkat, Anda dapat menggunakan device farm "Xcode Cloud" milik Apple atau mendistribusikan build pengujian via TestFlight untuk pengujian perangkat manual.
Bagaimana dengan pengujian pada versi iOS yang berbeda?
Anda dapat menginstal beberapa runtime iOS Simulator pada Mac yang sama. Gunakan xcodebuild -downloadPlatform iOS untuk yang terbaru, atau unduh runtime yang lebih lama dari Xcode Settings > Platforms. Kemudian tentukan versi OS dalam destinasi pengujian Anda: -destination 'platform=iOS Simulator,name=iPhone 16,OS=17.5'.