Section 8/141 menit
8. Testing Fitur AI: Strategi dan Mocking
8. Testing Fitur AI: Strategi dan Mocking
Tantangan Testing AI
AI non-deterministik — request yang sama bisa menghasilkan output berbeda. Strategi testing yang tepat berfokus pada behavior bukan exact output:
swift
// ❌ Testing AI: fragile — bergantung pada output exact
func testSentimentAnalysis() async throws {
let session = LanguageModelSession()
let result = try await session.respond(to: "Saya sangat senang!").content
XCTAssertEqual(result, "positif") // Akan gagal jika model menjawab "Positif" atau "Sentimen positif"
}
swift
// ✓ Testing AI: robust — test behavior dan struktur, bukan exact string
func testSentimentAnalysis() async throws {
guard case .available = SystemLanguageModel.default.availability else {
throw XCTSkip("Foundation Models tidak tersedia di environment ini")
}
@Generable
enum Sentiment: String { case positive, negative, neutral }
@Generable
struct SentimentResult {
var sentiment: Sentiment
}
let session = LanguageModelSession()
let result = try await session.respond(
to: "Analisis sentimen: 'Saya sangat senang hari ini!'",
generating: SentimentResult.self
).content
XCTAssertEqual(result.sentiment, .positive) // Enum-constrained — deterministik
}
Protocol-Based Mocking
Ekstrak dependency ke protocol untuk bisa mock saat testing:
swift
// Protocol yang merepresentasikan kemampuan AI session
protocol AISessionProtocol: Sendable {
func respond(to prompt: String) async throws -> String
}
// Wrapper tipis untuk LanguageModelSession nyata
final class RealAISession: AISessionProtocol {
private let session: LanguageModelSession
init(instructions: String? = nil) {
if let instructions {
session = LanguageModelSession(instructions: Instructions(instructions))
} else {
session = LanguageModelSession()
}
}
func respond(to prompt: String) async throws -> String {
try await session.respond(to: prompt).content
}
}
// Mock untuk testing
final class MockAISession: AISessionProtocol {
var responseQueue: [String] = []
var capturedPrompts: [String] = []
var shouldThrow: Error? = nil
func respond(to prompt: String) async throws -> String {
capturedPrompts.append(prompt)
if let error = shouldThrow { throw error }
return responseQueue.isEmpty ? "Mock response" : responseQueue.removeFirst()
}
}
// Service yang menggunakan protocol — bisa ditest
class ContentAnalysisService {
private let session: AISessionProtocol
init(session: AISessionProtocol = RealAISession()) {
self.session = session
}
func analyzeReadability(of text: String) async throws -> String {
let prompt = "Nilai keterbacaan teks ini (mudah/sedang/sulit): \(text.prefix(500))"
return try await session.respond(to: prompt)
}
}
// Test yang bisa berjalan tanpa model nyata
func testContentAnalysisUsesCorrectPrompt() async throws {
let mockSession = MockAISession()
mockSession.responseQueue = ["mudah"]
let service = ContentAnalysisService(session: mockSession)
let result = try await service.analyzeReadability(of: "Teks sederhana.")
XCTAssertEqual(result, "mudah")
XCTAssertTrue(mockSession.capturedPrompts.first?.contains("keterbacaan") == true)
}
Testing dengan Availability Check
swift
// Helper untuk skip test jika model tidak tersedia
extension XCTestCase {
func skipIfFoundationModelsUnavailable() throws {
guard case .available = SystemLanguageModel.default.availability else {
throw XCTSkip("Foundation Models tidak tersedia. Test ini membutuhkan Apple Intelligence.")
}
}
}
class FoundationModelsIntegrationTests: XCTestCase {
func testStructuredOutputIntegrity() async throws {
try skipIfFoundationModelsUnavailable()
@Generable
struct SimpleOutput {
@Guide(description: "Angka antara 1 dan 10")
var number: Int
}
let session = LanguageModelSession()
let result = try await session.respond(
to: "Beri angka acak antara 1 dan 10",
generating: SimpleOutput.self
).content
XCTAssertGreaterThanOrEqual(result.number, 1)
XCTAssertLessThanOrEqual(result.number, 10)
}
}