Smoke Testing for iOS Apps: Complete Guide (2026)
Smoke Testing for iOS Apps: Complete Guide (2026) is the practice of executing a minimal, high‑confidence set of checks immediately after a build to verify that the core functionality works and that n
Smoke Testing for iOS Apps: Complete Guide (2026) is the practice of executing a minimal, high‑confidence set of checks immediately after a build to verify that the core functionality works and that no obvious breakage blocks further testing. It acts as a gatekeeper: if the smoke suite fails, the build is rejected and deeper testing is postponed; if it passes, the team proceeds with confidence to functional, regression, or performance suites. The following guide walks through the definition, placement in the testing hierarchy, when and why to run it, a step‑by‑step process, tooling options, metrics, common mistakes, CI/CD integration, and how autonomous exploration can augment the effort. Each section contains concrete examples, tables, and code snippets that you can copy into your own projects.
1. What Is Smoke Testing for iOS Apps?
1.1 Definition and Scope
Smoke testing for iOS focuses on the *critical path* of an application: launch, authentication, primary navigation, and the most‑used feature (e.g., sending a message, viewing a feed, completing a purchase). The suite is intentionally small—usually 5‑15 test cases—so it can run in under two minutes on a physical device or simulator. Unlike sanity testing, which verifies a specific bug fix, smoke testing confirms that the build is *stable enough* to accept any further test effort.
1.2 Where It Sits in the Testing Pyramid
At the base of the pyramid sit unit tests; above them sit integration tests; UI‑level tests (including smoke) occupy the next tier; exploratory and acceptance tests sit at the top. Smoke tests are a subset of UI‑level tests that prioritize *speed* over *exhaustiveness*. They are not a replacement for regression suites; they are a *filter* that prevents wasted effort on obviously broken builds.
1.3 When to Execute Smoke Tests
- Pre‑commit: Run locally on a developer’s machine after a git commit to catch obvious regressions early.
- Post‑merge: Trigger automatically on the main branch after a pull request is merged.
- Pre‑release: Execute before handing off to QA or before a beta distribution (TestFlight) to ensure the build is installable and launchable.
- On‑demand: Run when a hot‑fix is applied to a production branch to verify that the patch did not break core flows.
1.4 Why Smoke Testing Matters for iOS
iOS apps are distributed through the App Store, where a single crash on launch can lead to immediate negative reviews and a drop in rating. Smoke testing catches:
- Missing or mislinked resources (e.g., launch storyboard, asset catalog errors).
- Broken entry points (e.g.,
application(_:didFinishLaunchingWithOptions:)returningfalse). - Failed authentication flows due to changed API keys or endpoint URLs.
- UI‑thread blockers that cause the app to hang on launch (often missed by unit tests).
- Accessibility regressions that make the app unusable for VoiceOver users on first launch.
2. Core Principles and Pass/Fail Criteria
2.1 Defining the Critical Path
Identify the *minimum* set of user actions that must succeed for the app to be considered usable. For a typical social‑media app, the critical path might be:
- App launches and shows the feed within 2 seconds.
- User can tap the login button, enter valid credentials, and be authenticated.
- After login, the home timeline loads at least one post.
- User can navigate to the profile tab and see their avatar.
- User can compose a new post and press “Send” without encountering an error.
Each step becomes a test case. If any step fails, the smoke suite fails.
2.2 Metrics That Matter
| Metric | Target | Measurement Tool |
|---|---|---|
| App launch time (cold start) | < 2 s (iPhone 14) | Xcode Instruments → Time Profiler |
| Success rate of critical path | 100 % (no flaky retries) | XCUITest test runner |
| Number of uncaught exceptions | 0 | Crashlytics or Firebase Crash Reporting |
| Accessibility audit failures (WCAG 2.1 AA) | 0 on launch screen | AXCore or Xcode Accessibility Inspector |
| Network error rate (critical endpoints) | < 1 % | Charles Proxy or Xcode Network Logger |
2.3 Pass/Fail Decision Logic
A smoke run is considered PASS when:
- All critical‑path test cases finish with a
successstatus. - No uncaught exception or crash is reported during the run.
- Launch time stays below the defined threshold.
- Zero new accessibility violations appear on the screens visited.
Any deviation triggers a FAIL. The CI system should automatically mark the build as unstable and notify the team via Slack or email.
2.4 Handling Flaky Tests
Flakiness undermines the gatekeeping purpose. Mitigation strategies include:
- Adding explicit waits for UI elements (
expectation(for: NSPredicate, evaluatedWith: handler:)). - Using deterministic test data (seed random number generators, mock network responses).
- Isolating the test device state (erase content and settings before each run).
- Retrying a failed step *once* only if the failure is due to a known transient issue (e.g., occasional network timeout) and logging the retry as a warning, not a pass.
3. Manual Smoke Testing Workflow
3.1 Building a Test Matrix
Even when automation is the goal, a manual smoke test matrix helps define the exact steps and expected outcomes. Below is an example matrix for a banking iOS app.
| Test ID | Step | Action | Expected Result | Data Needed |
|---|---|---|---|---|
| SMK‑001 | 1 | Tap app icon | App launches, splash screen shows for ≤ 800 ms | None |
| SMK‑002 | 2 | Wait for main screen | “Accounts Overview” visible, balance label non‑zero | None |
| SMK‑003 | 3 | Tap “Transfer” button | Transfer screen opens, source account dropdown populated | Two test accounts with sufficient funds |
| SMK‑004 | 4 | Enter amount “50.00”, select destination, tap “Continue” | Confirmation screen shows correct amount and accounts | Same as above |
| SMK‑005 | 5 | Tap “Confirm” | Alert shows “Transfer successful”, balance updated | Same as above |
| SMK‑006 | 6 | Tap “Logout” | Returns to login screen, fields cleared | None |
3.2 Executing the Matrix
- Device Preparation – Use a clean simulator or a physical device with the latest iOS version. Erase all content and settings (
simctl erase allor Settings → General → Transfer or Reset iPhone → Erase All Content and Settings). - Install Build – Drag the
.appbundle onto the simulator or usexcrun simctl install booted. - Launch – Open the app via the simulator hardware menu or by tapping the icon on the device.
- Follow Steps – Perform each action in the matrix, noting the actual result. Use a pen‑and‑paper checklist or a simple spreadsheet.
- Record Defects – If any step deviates, capture a screenshot, log the console output (
xcrun simctl spawn booted log show --predicate 'process == "YourApp"' --last 5m), and file a bug.
3.3 Advantages and Limitations
*Advantages*: Immediate feedback, no script maintenance, useful for exploratory checks (e.g., verifying a new UI animation).
*Limitations*: Time‑consuming, prone to human inconsistency, difficult to scale across multiple device configurations (different iOS versions, screen sizes, Dark/Light mode).
4. Automated Smoke Testing Approaches
4.1 Choosing a Framework
For iOS, the two dominant UI test frameworks are XCUITest (Apple‑native) and Appium (cross‑platform). XCUITest offers faster execution and deeper integration with Xcode; Appium enables reusing the same tests for Android and Web.
4.2 Basic XCUITest Smoke Suite
Create a new UI Test Target in Xcode (File → New → Target → UI Testing Bundle). Add a smoke test class:
import XCTest
final class SmokeTests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testLaunchAndLogin() throws {
// 1. Verify launch screen disappears within 2 seconds
let splash = app.images["LaunchScreen"]
XCTAssertTrue(splash.waitForExistence(timeout: 2.0), "Splash screen persisted")
XCTAssertFalse(splash.exists, "Splash screen should be gone")
// 2. Tap login button
let loginButton = app.buttons["Login"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5.0), "Login button not found")
loginButton.tap()
// 3. Enter credentials (using secure text fields)
let usernameField = app.textFields["Username"]
usernameField.tap()
usernameField.typeText("qa_user")
let passwordField = app.secureTextFields["Password"]
passwordField.tap()
passwordField.typeText("SecurePass!23")
// 4. Submit
let submitButton = app.buttons["Sign In"]
submitButton.tap()
// 5. Verify home screen appears
let homeLabel = app.staticTexts["Welcome, qa_user"]
XCTAssertTrue(homeLabel.waitForExistence(timeout: 10.0), "Home screen did not load")
}
}
Explanation:
continueAfterFailure = falsestops the test on the first failure, mimicking a gate.- Explicit waits (
waitForExistence) prevent flakiness due to animation timing. - The test uses accessibility identifiers (set in Interface Builder or code) to locate elements reliably.
4.3 Appium Equivalent (JavaScript)
If you prefer a single language stack, the same flow in Appium looks like:
const { driver } = require('appium');
const assert = require('assert');
describe('iOS Smoke Suite', function() {
let driver;
before(async function() {
driver = await driver.createSession({
platformName: 'iOS',
platformVersion: '17.4',
deviceName: 'iPhone 15',
app: '/path/to/YourApp.app',
automationName: 'XCUITest'
});
});
after(async function() {
await driver.deleteSession();
});
it('should launch and login successfully', async function() {
// Wait for splash to disappear
await driver.waitForElementByAccessibilityId('LaunchScreen', 2000, true);
const splashGone = !(await driver.isElementDisplayed('LaunchScreen'));
assert.strictEqual(splashGone, true, 'Splash screen still visible');
// Login flow
await driver.waitForElementByAccessibilityId('Login', 5000).click();
await driver.waitForElementByAccessibilityId('Username', 5000).sendKeys('qa_user');
await driver.waitForElementByAccessibilityId('Password', 5000).sendKeys('SecurePass!23');
await driver.waitForElementByAccessibilityId('Sign In', 5000).click();
// Verify home
const welcome = await driver.waitForElementByAccessibilityId('Welcome, qa_user', 10000);
assert.ok(await driver.isElementDisplayed(welcome), 'Home screen not shown');
});
});
4.4 Parallel Execution on Device Farm
To keep smoke runs under two minutes, distribute the test cases across multiple simulators or real devices using Xcode Cloud, Bitrise, or Firebase Test Lab. Example fastlane lane:
lane :smoke do
run_tests(
scheme: "YourAppUITests",
devices: ["iPhone 14", "iPhone 14 Pro"],
only_testing: ["YourAppUITests/SmokeTests/testLaunchAndLogin"],
concurrent_workers: 2,
output_types: "html,junit",
output_directory: "./fastlane/test_output"
)
end
The concurrent_workers flag runs the same test on two devices simultaneously, halving wall‑clock time.
5. Tooling Comparison Table
| Category | Tool | Language | Setup Complexity | Execution Speed (per smoke run) | CI Integration | Cost | Notable Features |
|---|---|---|---|---|---|---|---|
| Native UI | XCUITest | Swift/Objective‑C | Medium (requires Xcode project) | Fast (≈ 8‑12 s on iPhone 14 simulator) | Xcode Server, GitHub Actions, Bitrise | Free (included with Xcode) | Deep OS integration, access to private APIs, fastest startup |
| Cross‑platform | Appium (XCUITest driver) | Java, JS, Python, Ruby, C# | High (requires Appium server, desired caps) | Moderate (≈ 12‑18 s) | Jenkins, GitLab CI, Azure Pipelines | Free (open source) | Write once, run iOS/Android/Web, cloud device labs support |
| Low‑code | Waldo | No code (record‑play) | Low (record via Waldo dashboard) | Fast (≈ 10‑15 s) | GitHub, Bitrise, CircleCI | Freemium (paid for parallel runs) | No code maintenance, automatic UI change detection |
| Autonomous Exploration | SUSATest Agent | CLI (Python) | Low (pip install) | Variable (depends on exploration depth) | GitHub Actions, Bitrise, custom scripts | Free tier, paid for advanced personas | Generates Appium/Playwright scripts, multi‑persona testing, cross‑session learning |
| Cloud‑based Test Lab | Firebase Test Lab | Any (via gcloud or Firebase CLI) | Medium (requires Firebase project) | Variable (depends on device queue) | Firebase CI triggers, GitHub Actions | Pay‑per‑use | Real device matrix, video capture, performance profiling |
How to Choose
- If your team already maintains an Xcode project and wants the fastest feedback, start with XCUITest.
- If you need a single test suite for iOS, Android, and a web portal, Appium is the pragmatic choice.
- For teams lacking test engineering bandwidth, Waldo’s record‑and‑play can bootstrap smoke tests quickly, though you’ll eventually need to replace flaky recorded steps with coded assertions.
- When you want the smoke suite to evolve without manual maintenance, consider an autonomous agent like SUSATest that explores the app and generates regression scripts automatically.
6. Integrating Smoke Tests into CI/CD Pipelines
6.1 GitHub Actions Example (XCUITest)
Create .github/workflows/ios-smoke.yml:
name: iOS Smoke Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
smoke:
runs-on: macos-14
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -switch /Applications/Xcode_15.2.app
- name: Cache DerivedData
uses: actions/cache@v3
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-xcode-${{ hashFiles('**/*.xcodeproj') }}
restore-keys: |
${{ runner.os }}-xcode-
- name: Install Dependencies
run: |
brew install carthage
carthage bootstrap --platform iOS --no-use-binaries
- name: Build for Testing
run: |
xcodebuild -workspace YourApp.xcworkspace \
-scheme YourAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 14,OS=17.4' \
-enableCodeCoverage YES \
build-for-testing
- name: Run Smoke Tests
run: |
xcodebuild test-without-building \
-workspace YourApp.xcworkspace \
-scheme YourAppUITests \
-destination 'platform=iOS Simulator,name=iPhone 14,OS=17.4' \
-only-testing:SmokeTests/testLaunchAndLogin \
| xcpretty
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-results
path: ./**/test-results/**/*.xml
Key Points
- The
timeout-minutesensures the job fails fast if the simulator hangs. - Caching
DerivedDatacuts down build time on subsequent runs. only-testinglimits execution to the smoke test class, keeping the job under two minutes.- Artifacts store JUnit XML for downstream reporting (e.g., linking to pull‑request checks).
6.2 Bitrise Workflow (Appium)
Bitrise.yml snippet:
format_version: "13"
project_type: ios
default_step_lib_source: https://github.com/bitrise-io/bitrise-step-lib.git
app:
envs:
- opts:
is_expand: false
IOS_APP_PATH: "$BITRISE_SOURCE_DIR/YourApp.app"
- opts:
is_expand: false
APPIUM_VERSION: "2.0.0"
workflows:
smoke:
steps:
- git-clone: {}
- cache-pull: {}
- script@13:
- content: |
npm install -g appium@$APPIUM_VERSION
appium driver install xcuitest
- script@1:
title: "Start Appium Server"
inputs:
- content: |
appium --allow-insecure=chromedriver_autodownload &
sleep 5
- script@2:
title: "Run Appium Smoke Suite"
inputs:
- content: |
npx wdio wdio.conf.io.smoke.js
- deploy-to-bitrise-io: {}
This workflow installs Appium, launches the server, runs a WebdriverIO smoke spec, and publishes the results.
6.3 Triggering Smoke Tests on Hot‑Fix Branches
Add a branch‑specific rule in GitHub Actions:
on:
push:
branches:
- hotfix/*
This ensures that any emergency patch gets smoked before it is merged into main.
7. Common Mistakes and How to Avoid Them
| Mistake | Symptom | Root Cause | Prevention |
|---|---|---|---|
| Over‑reliance on hard‑coded coordinates | Tests fail on different device sizes or after UI tweaks | Using tapAtCoordinate or pressForDuration with fixed points | Always locate elements via accessibility identifiers; use relative gestures (swipeUp, pinch) if needed |
| Skipping device state reset | Leftover login state causes false passes | Not clearing user defaults, keychain, or app data between runs | In setUp, call XCUIApplication().launchArguments += ["-reset"] and implement a reset mode in the app, or use simctl spawn booted rm -rf ~/Library/Containers/ |
| Ignoring launch time variations | Sporadic failures blamed on “flaky network” | Not accounting for cold vs warm start differences | Measure launch time with XCTestMetric (XCTApplicationLaunchMetric) and assert it stays under threshold; run each test with a fresh simulator state |
| Using real network endpoints in smoke | Smoke suite fails intermittently due to backend downtime | Direct calls to production services | Stub or mock network layer for smoke (e.g., using URLProtocol subclass) or point to a dedicated staging endpoint with guaranteed uptime |
| Neglecting accessibility checks | Smoke passes but VoiceOver users cannot navigate | Accessibility not part of pass/fail criteria | Include an AXCore audit step (XCUIAccessibility) that scans the visited screens and fails on any WCAG violation |
| Treating smoke as a replacement for regression | Critical bugs slip through after smoke passes | Smoke only checks happy path; edge cases remain uncovered | Keep smoke as a gate; run a full regression suite on a separate schedule (nightly) or on feature branches |
| Failing to version‑control test data | Tests break after a backend schema change | Hard‑coded IDs or tokens that expire | Store test credentials in encrypted secrets (GitHub Secrets, Bitrise Secrets) and rotate them regularly; use dynamic test user creation via API if possible |
8. Leveraging Autonomous Exploration for Smarter Smoke Tests
8.1 What Autonomous Exploration Adds
Traditional smoke tests are static scripts that verify a predefined path. Autonomous exploration tools—such as the SUSATest agent—continuously interact with the app, discovering new screens, edge cases, and regressions without human‑written steps. The agent can:
- Generate a baseline exploration map after the first run.
- Learn which UI elements lead to dead ends or crashes.
- Produce ready‑to‑run Appium (Android) or Playwright (Web) scripts that mimic the discovered flows.
- Adapt over time, reducing flaky elements are deprioritized and new features are incorporated automatically.
8.2 Installing and Running the SUSATest Agent
# Install the CLI (requires Python 3.9+)
pip install susatest-agent
# Point it at a locally built .app or a TestFlight URL
susatest explore \
--app ./Build/Products/Debug-iphonesimulator/YourApp.app \
--device iPhone-14 \
--ios-version 17.4 \
--persona curious,impatient,elderly \
--output ./susatest-output \
--max-depth 5 \
--timeout 120
Explanation of flags
--deviceand--ios-versionselect the simulator or attached physical device.--personainjects behavior profiles (e.g., a curious persona taps every visible button; an impatient persona skips tutorials).--max-depthlimits how deep the agent will navigate from the launch screen to avoid infinite loops.--timeoutcaps total exploration time; a smoke‑oriented run might use 60‑90 seconds.
8.3 From Exploration to Smoke Scripts
After exploration completes, the agent creates a smoke_tests folder containing Appium scripts for each high‑confidence flow it observed. Example generated script (Python):
# generated by susatest-agent
from appium import webdriver
from appium.options.ios import XCUITestOptions
import time
options = XCUITestOptions()
options.platform_name = "iOS"
options.platform_version = "17.4"
options.device_name = "iPhone 14"
options.app = "/path/to/YourApp.app"
driver = webdriver.Remote("http://127.0.0.1:4723", options=options)
try:
# Launch screen wait
driver.find_element("accessibility id", "LaunchScreen")
time.sleep(2)
assert not driver.find_element("accessibility id", "LaunchScreen").is_displayed()
# Curious persona discovered: tap Settings → About → Version
driver.find_element("accessibility id", "SettingsButton").tap()
driver.find_element("accessibility id", "AboutRow").tap()
version = driver.find_element("accessibility id", "VersionLabel").text
assert "2.3.1" in version
# Impatient persona: skip onboarding by tapping "Get Started"
driver.find_element("accessibility id", "GetStartedButton").tap()
# Verify home appears
driver.find_element("accessibility id", "HomeFeed").is_displayed()
finally:
driver.quit()
You can then add this script to your CI pipeline as a *supplemental* smoke check. Because it is derived from actual exploration, it often catches flows that a manually written smoke suite would miss (e.g., a hidden promo banner that triggers a modal).
8.4 Cross‑Session Learning
The agent stores a JSON graph of visited screens and transitions in susatest-output/exploration_graph.json. On subsequent runs, it:
- Skips already‑known dead ends (screens that consistently lead to crashes).
- Prioritizes unexplored branches that have high interaction frequency in prior runs.
- Updates the generated smoke scripts automatically when the graph changes.
This reduces maintenance overhead: when a new feature is added, the agent will eventually discover it, and the next CI run will include a smoke check for that feature without any test author intervention.
8.5 Integrating SUSATest with Existing CI
Add a step after the build but before the traditional smoke suite:
- name: Run Autonomous Exploration (Smoke Augment)
run: |
susatest explore \
--app ./Build/Products/Debug-iphonesimulator/YourApp.app \
--device iPhone-14 \
--ios-version 17.4 \
--persona curious,elderly \
--output ./susatest-output \
--max-depth 4 \
--timeout 80
# Copy generated Appium scripts into the test repo
cp -r ./susatest-output/smoke_tests/* ./Tests/AppiumSmoke/
- name: Run Appium Smoke (including generated)
run: |
npx wdio wdio.conf.js --spec ./Tests/AppiumSmoke/**/*.js
The first step ensures that each build benefits from the latest exploration; the second step runs both handcrafted and machine‑generated smoke tests, giving you a hybrid safety net.
9. Quick Smoke‑Test Checklist (Copy‑Paste)
[ ] Clean simulator/device state (erase content & settings)
[ ] Install the latest .app (or download from TestFlight)
[ ] Launch app, verify splash screen disappears ≤ 2 s
[ ] Confirm main landing screen appears with expected UI elements
[ ] Execute primary login flow with valid test credentials
[ ] Verify post‑login landing page loads core content (feed, dashboard, etc.)
[ ] Navigate to each top‑level tab (home, profile, settings, etc.)
[ ] Perform the most‑used action (e.g., send message, start transaction)
[ ] Check for any uncaught exceptions or crashes in device console
[ ] Measure launch time; ensure it stays under defined threshold
[ ] Run accessibility audit on visited screens (WCAG 2.1 AA)
[ ] If any step fails → mark build FAILURE, notify team, halt further testing
[ ] If all steps pass → mark build PASS, promote to next test stage
Keep this checklist in your wiki or as a Markdown file in the repository; update it whenever the critical path changes.
10. Closing Takeaways
- Smoke testing for iOS is a *gate*, not a comprehensive test suite. Its value lies in speed and certainty: a pass tells you the build is installable, launchable, and capable of executing the core user journey.
- Define the critical path rigorously, automate it with XCUITest or Appium, and enforce quantitative thresholds (launch time, crash count, accessibility violations).
- Avoid common pitfalls such as hard‑coded coordinates, missing device state reset, and reliance on flaky network calls.
- Integrate smoke checks into every CI event that matters—pull‑request validation, post‑merge, and hot‑fix pipelines—using platform‑native GitHub Actions or Bitrise workflows.
- Augment manual and scripted smoke suites with autonomous exploration tools like SUSATest. They continuously discover new flows, generate regression‑ready scripts, and learn from past runs, reducing maintenance while increasing coverage.
- Treat the smoke suite as a living artifact: review it after each release, add new critical steps when the product evolves, and retire checks that become obsolete.
By following the steps, tables, and examples outlined above, you’ll have a reliable, fast‑acting smoke testing strategy that catches show‑stopper defects before they reach your users, keeps your CI pipeline green, and frees your QA team to focus on deeper exploratory and regression work. Happy testing.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free