Section 15/161 menit

15. Real Use Cases

15. Real Use Cases

Use Case 1: E-Commerce Order Processing Test Suite

Test suite komprehensif untuk order processing service dengan berbagai edge cases.

swift
import Testing

// Domain model
struct Order: Sendable {
    let id: String
    var items: [OrderItem]
    var status: OrderStatus
    var total: Decimal
}

enum OrderStatus: Sendable {
    case pending, confirmed, processing, shipped, delivered, cancelled
}

// Protocol untuk testability
protocol OrderRepository: Sendable {
    func save(_ order: Order) async throws
    func fetch(id: String) async throws -> Order
    func fetchAll(status: OrderStatus) async throws -> [Order]
}

// Fake repository
actor FakeOrderRepository: OrderRepository {
    private var orders: [String: Order] = [:]

    func save(_ order: Order) async throws {
        orders[order.id] = order
    }

    func fetch(id: String) async throws -> Order {
        guard let order = orders[id] else { throw OrderError.notFound }
        return order
    }

    func fetchAll(status: OrderStatus) async throws -> [Order] {
        orders.values.filter { $0.status == status }
    }
}

// Service
actor OrderService {
    private let repository: OrderRepository
    private let paymentGateway: PaymentGateway
    private let notifier: OrderNotifier

    init(repository: OrderRepository, paymentGateway: PaymentGateway, notifier: OrderNotifier) {
        self.repository = repository
        self.paymentGateway = paymentGateway
        self.notifier = notifier
    }

    func processOrder(_ order: Order) async throws -> Order {
        var mutableOrder = order
        let payment = try await paymentGateway.charge(amount: order.total)
        guard payment.success else { throw OrderError.paymentFailed }
        mutableOrder.status = .confirmed
        try await repository.save(mutableOrder)
        await notifier.sendConfirmation(for: mutableOrder)
        return mutableOrder
    }
}

// Test suite
@Suite("Order Processing")
struct OrderProcessingTests {
    var repository: FakeOrderRepository
    var paymentGateway: MockPaymentGateway
    var notifier: SpyOrderNotifier
    var service: OrderService

    init() async {
        repository = FakeOrderRepository()
        paymentGateway = MockPaymentGateway()
        notifier = SpyOrderNotifier()
        service = OrderService(
            repository: repository,
            paymentGateway: paymentGateway,
            notifier: notifier
        )
    }

    @Test("Order berhasil diproses saat payment sukses")
    func testSuccessfulOrderProcessing() async throws {
        paymentGateway.nextResult = .success

        let order = Order(id: "ORD-001", items: [.sample], status: .pending, total: 150000)
        let processed = try await service.processOrder(order)

        #expect(processed.status == .confirmed)

        let savedOrder = try await repository.fetch(id: "ORD-001")
        #expect(savedOrder.status == .confirmed)

        let notifiedCount = await notifier.confirmationsSent
        #expect(notifiedCount == 1)
    }

    @Test("Order gagal saat payment ditolak")
    func testFailedPayment() async throws {
        paymentGateway.nextResult = .declined

        let order = Order(id: "ORD-002", items: [.sample], status: .pending, total: 150000)

        #expect(throws: OrderError.paymentFailed) {
            try await service.processOrder(order)
        }

        // Order tidak tersimpan sebagai confirmed
        #expect(throws: OrderError.notFound) {
            try await repository.fetch(id: "ORD-002")
        }

        let notifiedCount = await notifier.confirmationsSent
        #expect(notifiedCount == 0)  // Tidak ada notifikasi saat gagal
    }

    @Test("Order dengan berbagai jumlah item", arguments: [1, 5, 10, 100])
    func testOrderWithVariousItemCounts(itemCount: Int) async throws {
        paymentGateway.nextResult = .success

        let items = (0..<itemCount).map { OrderItem(id: "\($0)", name: "Item \($0)", price: 10000) }
        let total = Decimal(itemCount) * 10000
        let order = Order(id: UUID().uuidString, items: items, status: .pending, total: total)

        let processed = try await service.processOrder(order)

        #expect(processed.status == .confirmed)
        #expect(processed.items.count == itemCount)
    }

    @Test("Notifikasi konfirmasi dikirim")
    func testConfirmationNotification() async throws {
        paymentGateway.nextResult = .success
        let order = Order(id: "ORD-003", items: [.sample], status: .pending, total: 50000)

        // Gunakan Confirmation untuk callback-based notifier
        await confirmation("Konfirmasi dikirim") { confirm in
            notifier.onConfirmationSent = { _ in confirm() }
            _ = try? await service.processOrder(order)
        }
    }
}

Use Case 2: Network Layer Testing dengan Berbagai Skenario Error

swift
import Testing
import Foundation

// Network layer yang testable
protocol HTTPClient: Sendable {
    func data(for request: URLRequest) async throws -> (Data, HTTPURLResponse)
}

// Fake HTTP client
actor FakeHTTPClient: HTTPClient {
    struct Response {
        let data: Data
        let statusCode: Int
    }

    var responseQueue: [Response] = []
    var defaultResponse = Response(data: Data(), statusCode: 200)

    func enqueue(_ response: Response) {
        responseQueue.append(response)
    }

    func data(for request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        let response = responseQueue.isEmpty ? defaultResponse : responseQueue.removeFirst()
        let httpResponse = HTTPURLResponse(
            url: request.url!,
            statusCode: response.statusCode,
            httpVersion: nil,
            headerFields: nil
        )!
        return (response.data, httpResponse)
    }
}

struct NetworkErrorCase: CustomTestStringConvertible {
    let statusCode: Int
    let expectedError: APIError
    var testDescription: String { "HTTP \(statusCode)\(expectedError)" }
}

@Suite("API Client Tests")
struct APIClientTests {

    @Test("Error HTTP status codes", arguments: [
        NetworkErrorCase(statusCode: 400, expectedError: .badRequest),
        NetworkErrorCase(statusCode: 401, expectedError: .unauthorized),
        NetworkErrorCase(statusCode: 403, expectedError: .forbidden),
        NetworkErrorCase(statusCode: 404, expectedError: .notFound),
        NetworkErrorCase(statusCode: 429, expectedError: .rateLimited),
        NetworkErrorCase(statusCode: 500, expectedError: .serverError),
        NetworkErrorCase(statusCode: 503, expectedError: .serviceUnavailable),
    ])
    func testHTTPErrorHandling(_ testCase: NetworkErrorCase) async throws {
        let fakeClient = FakeHTTPClient()
        await fakeClient.enqueue(.init(
            data: Data(),
            statusCode: testCase.statusCode
        ))

        let apiClient = APIClient(httpClient: fakeClient)

        #expect(throws: testCase.expectedError) {
            try await apiClient.fetchUser(id: "123")
        }
    }

    @Test("Retry pada error 429")
    func testRetryOn429() async throws {
        let fakeClient = FakeHTTPClient()

        // Dua kali 429, lalu sukses
        await fakeClient.enqueue(.init(data: Data(), statusCode: 429))
        await fakeClient.enqueue(.init(data: Data(), statusCode: 429))
        await fakeClient.enqueue(.init(
            data: try JSONEncoder().encode(User(id: "1", name: "Test")),
            statusCode: 200
        ))

        let apiClient = APIClient(httpClient: fakeClient, maxRetries: 3)
        let user = try await apiClient.fetchUser(id: "1")

        #expect(user.name == "Test")
    }

    @Test("Parse response JSON yang valid")
    func testValidJSONParsing() async throws {
        let expectedUser = User(id: "42", name: "Siti Rahayu", email: "siti@example.com")
        let fakeClient = FakeHTTPClient()

        await fakeClient.enqueue(.init(
            data: try JSONEncoder().encode(expectedUser),
            statusCode: 200
        ))

        let apiClient = APIClient(httpClient: fakeClient)
        let user = try await apiClient.fetchUser(id: "42")

        #expect(user.id == expectedUser.id)
        #expect(user.name == expectedUser.name)
        #expect(user.email == expectedUser.email)
    }

    @Test("Graceful handling invalid JSON")
    func testInvalidJSONResponse() async throws {
        let fakeClient = FakeHTTPClient()
        await fakeClient.enqueue(.init(
            data: Data("invalid json".utf8),
            statusCode: 200
        ))

        let apiClient = APIClient(httpClient: fakeClient)

        #expect(throws: APIError.decodingFailed) {
            try await apiClient.fetchUser(id: "1")
        }
    }
}