Swift Testing Guidelines
Testing prevents bugs from reaching production. XCTest validates synchronous code, async testing catches concurrency bugs, mocking isolates units, and ViewInspector tests SwiftUI views. Snapshot testing catches UI regressions. Without proper testing, race conditions, null dereferences, and logic errors reach production and cause crashes. Target: >85% code coverage for production applications, 100% coverage for critical business logic paths.
Overview
This guide covers comprehensive Swift testing strategies including XCTest patterns, async/await testing, mocking strategies without external frameworks, protocol-based test doubles, ViewInspector for SwiftUI testing, snapshot testing with swift-snapshot-testing, and testing best practices for iOS applications.
Core Principles
- XCTest Foundation: Use XCTest for unit and integration tests
- Async Testing: Test async/await code with proper expectations
- Protocol Mocks: Use protocols for dependency injection and mocking
- Test Doubles: Stubs, spies, and fakes for controlled testing
- SwiftUI Testing: ViewInspector for SwiftUI view testing
- Snapshot Testing: Visual regression testing for UI
- Fast Feedback: Unit tests complete in milliseconds
- Isolation: Each test independent and repeatable
- Readable Tests: Given-When-Then structure
- High Coverage: >85% for production applications
XCTest Basics
XCTest is Apple's official testing framework for Swift and Objective-C. It provides the infrastructure for writing unit tests, integration tests, and UI tests. XCTest integrates directly with Xcode, providing test discovery, execution, code coverage reporting, and integration with CI/CD pipelines.
The framework follows the xUnit testing pattern with setUp() and tearDown() lifecycle methods. Each test method must start with the word "test" for XCTest to discover it automatically. Tests run in isolation - each test method gets its own instance of the test class with fresh setup. This isolation prevents tests from affecting each other, making test order irrelevant and failures easier to diagnose.
The test lifecycle guarantees predictable test execution: setUp() runs before each test method, then the test executes, then tearDown() runs regardless of test success or failure. Without this lifecycle, failed tests leak resources or pollute state, causing subsequent tests to fail unpredictably. For comprehensive testing strategies, see our Testing Strategy guidelines.
Basic Test Structure
The System Under Test (SUT) pattern names the object being tested "sut" for clarity. Dependencies are mocked to isolate the code under test. The Given-When-Then structure makes tests readable - Given sets up preconditions, When executes the action, Then verifies outcomes:
// GOOD: Clear test structure
import XCTest
@testable import BankingApp
final class PaymentServiceTests: XCTestCase {
var sut: PaymentService! // System Under Test
var mockRepository: MockPaymentRepository!
var mockGateway: MockPaymentGateway!
override func setUp() {
super.setUp()
mockRepository = MockPaymentRepository()
mockGateway = MockPaymentGateway()
sut = PaymentService(
repository: mockRepository,
gateway: mockGateway
)
}
override func tearDown() {
sut = nil
mockRepository = nil
mockGateway = nil
super.tearDown()
}
func testProcessPayment_WithValidPayment_ReturnsSuccess() {
// Given
let payment = Payment(
id: "123",
amount: 100.00,
currency: "USD",
status: .pending
)
mockGateway.processResult = .success(PaymentReceipt(transactionId: "txn-456"))
// When
let result = sut.processPayment(payment)
// Then
XCTAssertEqual(result, .success(PaymentReceipt(transactionId: "txn-456")))
XCTAssertEqual(mockGateway.processCallCount, 1)
XCTAssertEqual(mockRepository.saveCallCount, 1)
}
func testProcessPayment_WithInvalidAmount_ThrowsError() {
// Given
let payment = Payment(
id: "123",
amount: -100.00,
currency: "USD",
status: .pending
)
// When/Then
XCTAssertThrowsError(try sut.processPayment(payment)) { error in
XCTAssertEqual(error as? PaymentError, .invalidAmount)
}
}
}
Assertions
// GOOD: Comprehensive assertions
func testPaymentCreation() {
let payment = Payment(
id: "123",
amount: 100.00,
currency: "USD",
status: .pending
)
// Equality
XCTAssertEqual(payment.id, "123")
XCTAssertEqual(payment.amount, 100.00)
// Boolean
XCTAssertTrue(payment.amount > 0)
XCTAssertFalse(payment.status == .completed)
// Nil checks
XCTAssertNotNil(payment.id)
XCTAssertNil(payment.completedAt)
// Comparisons
XCTAssertGreaterThan(payment.amount, 0)
XCTAssertLessThanOrEqual(payment.amount, 10000)
// Collections
let payments = [payment]
XCTAssertEqual(payments.count, 1)
XCTAssertTrue(payments.contains(payment))
}
Testing Errors
// GOOD: Testing thrown errors
func testValidatePayment_WithInvalidAmount_ThrowsError() {
let payment = Payment(amount: 0, currency: "USD")
XCTAssertThrowsError(try PaymentValidator.validate(payment)) { error in
guard let paymentError = error as? PaymentError else {
XCTFail("Expected PaymentError")
return
}
XCTAssertEqual(paymentError, .invalidAmount)
}
}
// GOOD: Testing no error thrown
func testValidatePayment_WithValidPayment_Succeeds() {
let payment = Payment(amount: 100, currency: "USD")
XCTAssertNoThrow(try PaymentValidator.validate(payment))
}
Async Testing
Testing async/await code in XCTest is straightforward - mark your test methods as async and use await to call async functions. Unlike the old approach with completion handlers and XCTestExpectation, async tests read linearly like synchronous code, making them easier to write and understand.
The key difference from non-async tests is that async test methods can suspend while waiting for operations to complete, without blocking the test runner. This allows efficient parallel test execution. When an async operation throws an error, the test framework catches it and fails the test with clear error information.
Async code without proper testing hides race conditions, concurrency bugs, and error propagation failures that only manifest under production load. Since async/await eliminates callback-based code, tests become more deterministic and easier to debug. For async patterns in production code, see our Swift General guidelines.
Testing Async Functions
Async test methods use the same async throws syntax as production code. You can directly await results and use standard XCTest assertions:
// GOOD: Testing async/await functions
final class PaymentServiceAsyncTests: XCTestCase {
func testFetchPayments_ReturnsPayments() async throws {
// Given
let mockRepository = MockPaymentRepository()
let expectedPayments = [
Payment(id: "1", amount: 100, currency: "USD"),
Payment(id: "2", amount: 200, currency: "USD")
]
mockRepository.paymentsToReturn = expectedPayments
let sut = PaymentService(repository: mockRepository)
// When
let payments = try await sut.fetchPayments()
// Then
XCTAssertEqual(payments, expectedPayments)
XCTAssertEqual(mockRepository.fetchCallCount, 1)
}
func testProcessPayment_WithNetworkError_ThrowsError() async {
// Given
let mockGateway = MockPaymentGateway()
mockGateway.shouldThrowError = true
let sut = PaymentService(gateway: mockGateway)
let payment = Payment(id: "123", amount: 100, currency: "USD")
// When/Then
do {
_ = try await sut.processPayment(payment)
XCTFail("Expected error to be thrown")
} catch {
XCTAssertEqual(error as? PaymentError, .networkError)
}
}
}
Testing Concurrent Operations
// GOOD: Testing concurrent async operations
func testProcessPayments_ProcessesMultipleConcurrently() async throws {
// Given
let payments = [
Payment(id: "1", amount: 100, currency: "USD"),
Payment(id: "2", amount: 200, currency: "USD"),
Payment(id: "3", amount: 300, currency: "USD")
]
let mockGateway = MockPaymentGateway()
mockGateway.processDelay = 0.1 // Simulate network delay
let sut = PaymentService(gateway: mockGateway)
let startTime = Date()
// When
let receipts = try await sut.processPayments(payments)
let duration = Date().timeIntervalSince(startTime)
// Then
XCTAssertEqual(receipts.count, 3)
// If processed sequentially: 0.3s, if concurrent: ~0.1s
XCTAssertLessThan(duration, 0.2, "Should process concurrently")
}
Testing with Expectations
// GOOD: Using expectations for async callbacks
func testPaymentNotification_IsReceived() {
// Given
let expectation = expectation(description: "Payment notification received")
let notificationCenter = NotificationCenter.default
let observer = notificationCenter.addObserver(
forName: .paymentCompleted,
object: nil,
queue: .main
) { notification in
expectation.fulfill()
}
// When
sut.completePayment()
// Then
wait(for: [expectation], timeout: 1.0)
notificationCenter.removeObserver(observer)
}
Mocking and Test Doubles
Test doubles are objects that stand in for real dependencies during testing. Swift's protocol-oriented design makes test doubles natural - define protocols for dependencies, use real implementations in production, and use test doubles in tests. This eliminates the need for complex mocking frameworks common in other languages.
There are three main types of test doubles: mocks (verify behavior by recording method calls), stubs (provide canned responses), and fakes (working implementations with shortcuts, like in-memory databases). Choose based on what you're testing - use mocks to verify interactions, stubs to control inputs, and fakes for complex dependencies that need realistic behavior.
Protocol-based test doubles provide compile-time safety that mocking frameworks can't match. If you change a protocol method signature, all implementations (production and test) must update. This catches breaking changes immediately rather than discovering them at runtime. For architectural patterns that leverage protocols, see our iOS Architecture guidelines.
Protocol-Based Mocks
Protocols define the contract, production code implements it, and test code uses mock implementations. Mocks track call counts and arguments to verify behavior:
// GOOD: Protocol for dependency injection
protocol PaymentRepository {
func findById(_ id: String) async throws -> Payment?
func save(_ payment: Payment) async throws
func findAll() async throws -> [Payment]
}
// Production implementation
final class CoreDataPaymentRepository: PaymentRepository {
func findById(_ id: String) async throws -> Payment? {
// Core Data implementation
}
func save(_ payment: Payment) async throws {
// Core Data implementation
}
func findAll() async throws -> [Payment] {
// Core Data implementation
}
}
// Test mock
final class MockPaymentRepository: PaymentRepository {
var findByIdCallCount = 0
var saveCallCount = 0
var findAllCallCount = 0
var paymentToReturn: Payment?
var paymentsToReturn: [Payment] = []
var shouldThrowError = false
func findById(_ id: String) async throws -> Payment? {
findByIdCallCount += 1
if shouldThrowError {
throw PaymentError.notFound
}
return paymentToReturn
}
func save(_ payment: Payment) async throws {
saveCallCount += 1
if shouldThrowError {
throw PaymentError.saveFailed
}
}
func findAll() async throws -> [Payment] {
findAllCallCount += 1
if shouldThrowError {
throw PaymentError.fetchFailed
}
return paymentsToReturn
}
}
Spy for Verifying Calls
// GOOD: Spy to verify method calls and arguments
protocol PaymentGateway {
func process(_ payment: Payment) async throws -> PaymentReceipt
}
final class SpyPaymentGateway: PaymentGateway {
private(set) var processedPayments: [Payment] = []
var receiptToReturn: PaymentReceipt?
func process(_ payment: Payment) async throws -> PaymentReceipt {
processedPayments.append(payment)
guard let receipt = receiptToReturn else {
throw PaymentError.gatewayError
}
return receipt
}
func verifyProcessed(paymentId: String) -> Bool {
return processedPayments.contains { $0.id == paymentId }
}
func reset() {
processedPayments.removeAll()
}
}
// Usage in test
func testProcessPayment_CallsGateway() async throws {
// Given
let spy = SpyPaymentGateway()
spy.receiptToReturn = PaymentReceipt(transactionId: "txn-123")
let sut = PaymentService(gateway: spy)
let payment = Payment(id: "123", amount: 100, currency: "USD")
// When
_ = try await sut.processPayment(payment)
// Then
XCTAssertTrue(spy.verifyProcessed(paymentId: "123"))
XCTAssertEqual(spy.processedPayments.count, 1)
}
Fake for Complex Behavior
// GOOD: Fake implementation for testing
final class FakePaymentRepository: PaymentRepository {
private var payments: [String: Payment] = [:]
func findById(_ id: String) async throws -> Payment? {
return payments[id]
}
func save(_ payment: Payment) async throws {
payments[payment.id] = payment
}
func findAll() async throws -> [Payment] {
return Array(payments.values)
}
func delete(_ id: String) {
payments.removeValue(forKey: id)
}
func reset() {
payments.removeAll()
}
}
// Usage in test
func testPaymentWorkflow_SavesAndRetrieves() async throws {
// Given
let fake = FakePaymentRepository()
let sut = PaymentService(repository: fake)
let payment = Payment(id: "123", amount: 100, currency: "USD")
// When
try await sut.createPayment(payment)
let retrieved = try await sut.getPayment(id: "123")
// Then
XCTAssertEqual(retrieved, payment)
}
SwiftUI Testing with ViewInspector
SwiftUI's declarative nature makes traditional UI testing challenging - views are value types that describe UI rather than being UI objects you can query. ViewInspector solves this by providing a way to inspect SwiftUI view hierarchies programmatically, enabling unit testing of views without running the full UI.
ViewInspector works by traversing the SwiftUI view tree and exposing view content for assertions. You can verify text content, button states, list items, navigation structure, and more. This is significantly faster than UI tests (which launch the full app) and more reliable (no timing issues or flakiness from animations).
The key insight is that SwiftUI views are just Swift types, so they're testable like any other code. ViewInspector bridges the gap between SwiftUI's internal representation and test assertions. For broader UI testing strategies including UI tests, see our iOS Testing guidelines and SwiftUI patterns.
Testing Views
ViewInspector lets you call .inspect() on any SwiftUI view to query its content. You can find specific views by type, verify their properties, and assert on their state:
import ViewInspector
import SwiftUI
import XCTest
// GOOD: Testing SwiftUI views with ViewInspector
final class PaymentListViewTests: XCTestCase {
func testPaymentListView_DisplaysPayments() throws {
// Given
let payments = [
Payment(id: "1", recipientName: "John Doe", amount: 100, currency: "USD"),
Payment(id: "2", recipientName: "Jane Smith", amount: 200, currency: "USD")
]
let view = PaymentListView(payments: payments)
// When
let list = try view.inspect().list()
// Then
XCTAssertEqual(try list.count(), 2)
let firstRow = try list.row(0).view(PaymentRow.self)
XCTAssertEqual(try firstRow.actualView().payment.recipientName, "John Doe")
}
func testPaymentListView_EmptyState_ShowsMessage() throws {
// Given
let view = PaymentListView(payments: [])
// When
let text = try view.inspect().find(text: "No payments yet")
// Then
XCTAssertNotNil(text)
}
}
Testing User Interactions
// GOOD: Testing button taps
final class PaymentButtonTests: XCTestCase {
func testPaymentButton_WhenTapped_CallsAction() throws {
// Given
var actionCalled = false
let view = Button("Pay Now") {
actionCalled = true
}
// When
try view.inspect().button().tap()
// Then
XCTAssertTrue(actionCalled)
}
func testPaymentForm_SubmitButton_IsDisabled_WhenAmountInvalid() throws {
// Given
let viewModel = PaymentViewModel()
viewModel.amount = "" // Invalid amount
let view = PaymentFormView(viewModel: viewModel)
// When
let button = try view.inspect().find(button: "Submit")
// Then
XCTAssertTrue(try button.isDisabled())
}
}
Testing ViewModels with SwiftUI
// GOOD: Testing ViewModel state changes
final class PaymentViewModelTests: XCTestCase {
@MainActor
func testLoadPayments_UpdatesState() async throws {
// Given
let mockRepository = MockPaymentRepository()
mockRepository.paymentsToReturn = [
Payment(id: "1", amount: 100, currency: "USD")
]
let viewModel = PaymentViewModel(repository: mockRepository)
XCTAssertEqual(viewModel.payments.count, 0)
XCTAssertFalse(viewModel.isLoading)
// When
await viewModel.loadPayments()
// Then
XCTAssertEqual(viewModel.payments.count, 1)
XCTAssertFalse(viewModel.isLoading)
}
@MainActor
func testLoadPayments_WithError_SetsErrorState() async throws {
// Given
let mockRepository = MockPaymentRepository()
mockRepository.shouldThrowError = true
let viewModel = PaymentViewModel(repository: mockRepository)
// When
await viewModel.loadPayments()
// Then
XCTAssertNotNil(viewModel.errorMessage)
XCTAssertTrue(viewModel.errorMessage!.contains("error"))
}
}
Snapshot Testing
Snapshot testing captures a reference image of your UI and compares future test runs against that reference. When UI changes occur, the test fails if the rendered output doesn't match the snapshot. This catches unintended visual regressions that assertions alone might miss - layout changes, styling bugs, or rendering issues.
The workflow is: run the test once to record the reference snapshot, then subsequent runs compare against that reference. When you intentionally change UI, you re-record the snapshot to update the reference. This makes snapshot tests self-documenting - the snapshots serve as visual documentation of your UI's expected appearance.
Snapshot tests are particularly valuable for complex layouts, custom controls, and scenarios where precise layout is critical. They complement traditional assertion-based tests by verifying the entire visual output, not just individual properties. However, snapshots can be brittle if not managed carefully - small, intentional changes require updating many snapshots. For comprehensive testing approaches, see our Testing Strategy guidelines.
Basic Snapshot Tests
The swift-snapshot-testing library provides a flexible snapshot testing framework. You can snapshot SwiftUI views, UIKit views, entire view controllers, and even non-visual types (JSON, strings, etc.):
import SnapshotTesting
import SwiftUI
import XCTest
// GOOD: Snapshot testing for UI
final class PaymentCardSnapshotTests: XCTestCase {
func testPaymentCard_CompletedStatus() {
let payment = Payment(
id: "123",
recipientName: "John Doe",
amount: 100.00,
currency: "USD",
status: .completed
)
let view = PaymentCard(payment: payment)
.frame(width: 375) // iPhone width
assertSnapshot(matching: view, as: .image)
}
func testPaymentCard_PendingStatus() {
let payment = Payment(
id: "123",
recipientName: "John Doe",
amount: 100.00,
currency: "USD",
status: .pending
)
let view = PaymentCard(payment: payment)
.frame(width: 375)
assertSnapshot(matching: view, as: .image)
}
func testPaymentCard_DarkMode() {
let payment = Payment(
id: "123",
recipientName: "John Doe",
amount: 100.00,
currency: "USD",
status: .completed
)
let view = PaymentCard(payment: payment)
.frame(width: 375)
.preferredColorScheme(.dark)
assertSnapshot(matching: view, as: .image)
}
}
Snapshot Testing Multiple Configurations
// GOOD: Test multiple device sizes and configurations
func testPaymentScreen_MultipleConfigurations() {
let payment = Payment(
id: "123",
recipientName: "John Doe",
amount: 100.00,
currency: "USD",
status: .completed
)
let view = PaymentDetailView(payment: payment)
// Test different device sizes
assertSnapshot(matching: view, as: .image(on: .iPhone13))
assertSnapshot(matching: view, as: .image(on: .iPhone13Pro))
assertSnapshot(matching: view, as: .image(on: .iPhoneSe))
// Test accessibility sizes
assertSnapshot(
matching: view,
as: .image(on: .iPhone13, traits: .init(preferredContentSizeCategory: .extraLarge))
)
// Test landscape
assertSnapshot(
matching: view,
as: .image(on: .iPhone13, orientation: .landscape)
)
}
Integration Testing
Integration tests verify that multiple components work together correctly, as opposed to unit tests which test components in isolation. For iOS apps, a common integration test scenario is validating network layer interactions without making real network calls. URLProtocol provides the mechanism to mock network responses at the URLSession level.
URLProtocol is a powerful abstraction in Foundation's networking stack that allows intercepting and handling URL loading. By creating a custom URLProtocol subclass for testing, you can control exactly what responses URLSession returns, enabling deterministic tests of network code without hitting real APIs. This is faster than real network calls and doesn't require network connectivity or running mock servers.
This approach tests the full networking stack - URL construction, request configuration, response parsing, error handling - with the only mock being the actual network transport. It catches bugs that pure unit tests might miss, like JSON decoding issues or unexpected response formats. For broader integration testing patterns, see our Integration Testing guidelines.
Testing with URLSession
Create a custom URLProtocol that intercepts requests and returns controlled responses. Register this protocol with a URLSession configuration used in tests:
// GOOD: Testing network layer with URLProtocol mock
final class MockURLProtocol: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override class func canInit(with request: URLRequest) -> Bool {
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
guard let handler = MockURLProtocol.requestHandler else {
fatalError("Handler not set")
}
do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
// Usage in test
final class PaymentAPITests: XCTestCase {
func testFetchPayments_ReturnsData() async throws {
// Given
let config = URLSessionConfiguration.ephemeral
config.protocolClasses = [MockURLProtocol.self]
let session = URLSession(configuration: config)
MockURLProtocol.requestHandler = { request in
let response = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil
)!
let json = """
[
{
"id": "123",
"amount": "100.00",
"currency": "USD",
"status": "completed"
}
]
"""
return (response, json.data(using: .utf8)!)
}
let api = PaymentAPI(session: session)
// When
let payments = try await api.fetchPayments()
// Then
XCTAssertEqual(payments.count, 1)
XCTAssertEqual(payments.first?.id, "123")
}
}
Testing Best Practices
Well-organized tests are easier to maintain, understand, and extend. As test suites grow, organization becomes critical - a disorganized test file is as problematic as disorganized production code. Structure tests logically, use consistent naming, and create reusable test utilities to reduce duplication.
Test naming should be descriptive enough that failures are self-explanatory. The pattern test[MethodName]_[Scenario]_[ExpectedResult] makes it clear what's being tested, under what conditions, and what should happen. MARK comments group related tests, making navigation easier in large test files.
Test data builders eliminate boilerplate object creation. Instead of repeating the same complex object initialization in every test, builders provide sensible defaults with the ability to override specific properties. This makes tests more readable and maintainable - when the object structure changes, you update the builder once rather than hundreds of tests.
Test Organization
Use MARK comments to create logical sections, group related tests together, and follow consistent naming patterns. This makes large test files navigable:
// GOOD: Organized test suite with MARK comments
final class PaymentServiceTests: XCTestCase {
// MARK: - Properties
var sut: PaymentService!
var mockRepository: MockPaymentRepository!
var mockGateway: MockPaymentGateway!
// MARK: - Lifecycle
override func setUp() {
super.setUp()
mockRepository = MockPaymentRepository()
mockGateway = MockPaymentGateway()
sut = PaymentService(
repository: mockRepository,
gateway: mockGateway
)
}
override func tearDown() {
sut = nil
mockRepository = nil
mockGateway = nil
super.tearDown()
}
// MARK: - Process Payment Tests
func testProcessPayment_WithValidPayment_ReturnsSuccess() {
// Test implementation
}
func testProcessPayment_WithInvalidAmount_ThrowsError() {
// Test implementation
}
// MARK: - Cancel Payment Tests
func testCancelPayment_WithPendingPayment_Succeeds() {
// Test implementation
}
func testCancelPayment_WithCompletedPayment_Fails() {
// Test implementation
}
}
Test Data Builders
// GOOD: Test data builders for reusable test objects
struct PaymentTestBuilder {
static func makePayment(
id: String = "test-123",
recipientName: String = "John Doe",
amount: Decimal = 100.00,
currency: String = "USD",
status: PaymentStatus = .pending
) -> Payment {
return Payment(
id: id,
recipientName: recipientName,
amount: amount,
currency: currency,
status: status,
createdAt: Date()
)
}
static func makePendingPayment(amount: Decimal = 100.00) -> Payment {
return makePayment(amount: amount, status: .pending)
}
static func makeCompletedPayment(amount: Decimal = 100.00) -> Payment {
return makePayment(amount: amount, status: .completed)
}
}
// Usage
func testProcessPayment() {
let payment = PaymentTestBuilder.makePendingPayment(amount: 250.00)
// Test logic
}
Testing Private Methods (When Necessary)
// GOOD: Testing through public interface
// Test private methods indirectly through public methods
final class PaymentValidator {
func validate(_ payment: Payment) throws {
try validateAmount(payment.amount)
try validateCurrency(payment.currency)
}
private func validateAmount(_ amount: Decimal) throws {
guard amount > 0 else {
throw PaymentError.invalidAmount
}
}
private func validateCurrency(_ currency: String) throws {
guard currency.count == 3 else {
throw PaymentError.invalidCurrency
}
}
}
// Test validates private methods through public interface
func testValidate_WithInvalidAmount_ThrowsError() {
let payment = Payment(amount: -100, currency: "USD")
XCTAssertThrowsError(try PaymentValidator().validate(payment))
}
Further Reading
Internal Documentation
- Swift General - Swift language guidelines
- iOS Testing - iOS testing strategies
- Testing Strategy - Overall testing approach
External Resources
Summary
Key Takeaways
- XCTest - Foundation for unit and integration tests
- Async testing - Test async/await with proper expectations
- Protocol mocks - Use protocols for dependency injection
- Test doubles - Mocks, spies, and fakes for controlled testing
- ViewInspector - Test SwiftUI views programmatically
- Snapshot testing - Visual regression testing for UI
- Given-When-Then - Clear test structure
- Test builders - Reusable test data creation
- Integration tests - Test with URLProtocol mocks
- High coverage - Target >85% for banking applications
Next Steps: Review iOS Testing for comprehensive iOS testing strategies and Testing Strategy for overall testing approach.