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