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")
}
}
}