Testing with XCTest and Swift Testing
Test targets, the expect and require macros, async tests, parameterised cases, protocol-based mocks, UI tests and running everything in CI.
Swift Testing
import Testing
struct PriceTests {
@Test("applies discount before tax")
func discountBeforeTax() {
let calculator = PriceCalculator(taxRate: 0.2)
#expect(calculator.total(unitPrice: 100, quantity: 3, discount: 0.1) == 324)
}
@Test("rejects negative quantities", arguments: [-1, -50, Int.min])
func rejectsNegative(quantity: Int) {
let calculator = PriceCalculator(taxRate: 0.2)
#expect(throws: PriceError.self) {
try calculator.total(unitPrice: 100, quantity: quantity, discount: 0)
}
}
@Test("round trips through JSON")
func codableRoundTrip() throws {
let original = Article.fixture()
let data = try JSONEncoder().encode(original)
let decoded = try JSONDecoder().decode(Article.self, from: data)
#expect(decoded == original)
}
@Test(.timeLimit(.minutes(1)))
@MainActor
func viewModelPublishesAfterLoad() async {
let model = FeedViewModel(api: StubAPI(articles: [.fixture()]))
await model.load()
let count = model.articles.count
#expect(count == 1)
}
}#expectrecords a failure and continues the test;#requireunwraps an optional or throws to stop immediately.- Tests run in parallel by default, so a test must not depend on another test having run.
- Parameterised arguments cover a table of inputs without duplicated method bodies.
@Suitegroups related tests and can carry traits, such as serialising a suite that touches shared files.
XCTest, mocks and UI tests
import XCTest
final class ArticleRepositoryTests: XCTestCase {
func testFallsBackToCacheWhenOffline() async throws {
let api = StubAPI(error: URLError(.notConnectedToInternet))
let cache = InMemoryCache(articles: [.fixture(id: 1)])
let repository = ArticleRepository(api: api, cache: cache)
let articles = try await repository.recent()
XCTAssertEqual(articles.map(\.id), [1])
}
func testMeasureDecode() throws {
let data = try Fixture.data(named: "articles.json")
measure { _ = try? JSONDecoder().decode([Article].self, from: data) }
}
}
final class CheckoutUITests: XCTestCase {
func testAddToCartUpdatesTheBadge() {
let app = XCUIApplication()
app.launchArguments = ["-uiTestSeed", "empty"]
app.launch()
app.buttons["Add to cart"].firstMatch.tap()
let badge = app.staticTexts["cart-count"]
XCTAssertTrue(badge.waitForExistence(timeout: 5))
XCTAssertEqual(badge.label, "1")
}
}| Framework | Style | Best for |
|---|---|---|
| Swift Testing | Macros and value types | New unit and integration tests |
| XCTest | Class-based with assertions | UI tests and existing suites |
| Both together | Mixed in one target | Incremental migration |
A protocol and a stub is usually enough. Reach for a generated mock only when you need to verify call order or argument values across many interactions, and even then prefer asserting on observable outcomes.
Running tests in CI
# packages: fast, no simulator needed
swift test --parallel --enable-code-coverage
# Apple apps: a specific simulator, result bundle for the report
xcodebuild test \
-scheme App -destination 'platform=iOS Simulator,name=iPhone 16' \
-enableCodeCoverage YES -resultBundlePath build/TestResults.xcresult
xcrun xccov view --report --json build/TestResults.xcresult > coverage.json⚠️
An async test that awaits a real network call is flaky by construction. Inject a stub client, use a virtual clock for anything time based, and keep a single end-to-end smoke test that is allowed to be slower and is retried rather than trusted.
FAQ
Should I migrate from XCTest to Swift Testing?
Write new tests with Swift Testing and migrate the unit tests that benefit from parameterisation or the cleaner assertions. UI tests stay in XCTest for now, and both can live in the same target during the transition.
How do I test code that uses <code>Date.now</code> directly?
Inject a clock protocol with a
now property and use a fixed implementation in tests. Code that reads the system clock inline cannot be tested deterministically without freezing time globally.Related
Networking, persistence and Codable Packaging, build configuration and distribution
Last refreshed 2026-09-18.