This episode covers quality assurance in Swift: unit testing with XCTest, UI testing and snapshot testing, mocking dependencies with test doubles, plus setting up continuous integration for Swift projects so quality is guaranteed automatically.

Tests aren't a tax you have to pay — they're a safety net that lets you change code with confidence. Episode 15 covers testing and quality assurance in Swift: unit testing with XCTest, UI testing and snapshot testing, mocking dependencies, and continuous integration so quality holds on every change.
The combination of good tests and CI that runs them automatically changes how a team works: bugs are found within minutes of the code landing, not weeks later in the hands of users.
XCTest is Apple's standard testing framework. The SPM package from episode 10 already includes a test target; tests are written in classes that inherit from XCTestCase:
import XCTest
final class KalkulatorTests: XCTestCase {
func testTambahPositif() {
let hasil = tambah(2, 3)
XCTAssertEqual(hasil, 5)
}
func testTambahNegatif() {
XCTAssertEqual(tambah(-1, 1), 0)
}
}XCTAssertEqual(hasil, 5) compares the actual and expected values. Test names like testTambahPositif follow a convention that's easy to read in reports. Each test runs independently — one failing doesn't affect the others.
Tests run from the terminal or Xcode:
swift testswift test builds the package and runs all tests in the test targets. For a library package, this is the fastest way to validate changes. In Xcode, press Cmd-U to run the active test target.
XCUITest tests the app from the outside — like a user tapping and swiping on a real screen:
final class AplikasiUITests: XCTestCase {
func testLoginTampil() {
let app = XCUIApplication()
app.launch()
let tombol = app.buttons["loginButton"]
XCTAssertTrue(tombol.exists)
tombol.tap()
}
}app.buttons["loginButton"] finds a UI element by accessibility identifier, then tombol.tap() simulates a tap. UI tests are slow and sensitive to layout changes — use them for critical flows like login and checkout, not for every screen.
Snapshot testing compares a UI's appearance against reference images:
swift test --filter SnapshotTestsswift test --filter SnapshotTests runs only the tests matching the filter. Snapshots work like visual tests: an unintended change in appearance is caught immediately. Update snapshots explicitly when a visual change is intentional — never without reviewing the diff.
Unit tests must run fast and deterministically — without real networking, real time, or random data. That's what test doubles are for: fake objects that replace real dependencies. Here's an example using a protocol as the contract:
protocol LayananJaringan {
func ambilData() async throws -> Data
}
struct LayananAsli: LayananJaringan {
func ambilData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(
from: URL(string: "https://api.example.com")!)
return data
}
}
struct LayananTiruan: LayananJaringan {
var hasilData: Data
func ambilData() async throws -> Data {
return hasilData
}
}struct LayananTiruan: LayananJaringan implements the same contract without networking. Production code depends on the protocol, so tests can inject the fake whenever needed — this pattern keeps the logic under test free of external factors.
Know the four main types:
CI runs build and tests on every push. For server-side Swift projects, the workflow is simple:
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0"
- run: swift build
- run: swift testuses: swift-actions/setup-swift@v2 installs the Swift toolchain on the CI runner. For iOS apps, builds use xcodebuild test on a macOS runner, and episode 18 will cover build tooling thoroughly.
Consistent quality needs standards enforced automatically:
Tip
Start CI early in the project, not after it's big. Small automation running from day one is far easier to maintain than re-engineering a pipeline once the project is complicated.
Key takeaways:
XCTAssertEqual and friends.swift test runs all package tests from the terminal.In the next episode, episode 16, we'll cover architecture and design patterns — MVC, MVVM, MVP, and Clean Architecture in Swift, protocol-oriented design and dependency injection, the coordinator pattern for navigation, and state management with Combine and SwiftUI. Your architecture will hold up as the app grows!