FS
ForgeStorage

Testing

Every store protocol has an in-memory implementation that requires no disk I/O, no Keychain entitlements, and no cleanup. Inject these via the protocol and your tests run fast, isolated, and deterministically.

In-Memory Implementations

Protocol Test Implementation Behavior
KVStoring InMemoryKVStore Dictionary-backed, thread-safe
FileStoring InMemoryFileStore Dictionary-backed, no disk I/O
CryptStoring InMemoryCrypt Dictionary-backed, no entitlement needed

Protocol-Based Injection

Depend on protocols, not concrete types. This lets you swap in-memory implementations for tests.

TaskFlowSettingsService.swift
// Production code — depends on protocol
final class TaskFlowSettingsService {
    private let store: KVStoring

    init(store: KVStoring) {
        self.store = store
    }

    var hasCompletedOnboarding: Bool {
        store.get(TaskKeys.onboarding) ?? false
    }

    func completeOnboarding() throws {
        try store.set(TaskKeys.onboarding, value: true)
    }

    func reset() throws {
        try store.removeAll()
    }
}
TaskFlowSettingsServiceTests.swift
// Test — inject InMemoryKVStore
import Testing
@testable import TaskFlow

struct TaskFlowSettingsServiceTests {
    let store = InMemoryKVStore()
    let service: TaskFlowSettingsService

    init() {
        service = TaskFlowSettingsService(store: store)
    }

    @Test func defaultOnboardingState() {
        #expect(!service.hasCompletedOnboarding)
    }

    @Test func completeOnboarding() throws {
        try service.completeOnboarding()
        #expect(service.hasCompletedOnboarding)
    }

    @Test func resetClearsAllData() throws {
        try service.completeOnboarding()
        try service.reset()
        #expect(!service.hasCompletedOnboarding)
    }
}

Testing Key-Value Storage

Basic Operations

KeyValueTests.swift
import Testing
import ForgeKVStore

struct KeyValueTests {
    let store = InMemoryKVStore()

    @Test func getReturnsNilWhenNotSet() {
        let key = KVKey<Int>("taskCount")
        #expect(store.get(key) == nil)
    }

    @Test func setAndGet() throws {
        let key = KVKey<String>("projectName")
        try store.set(key, value: "TaskFlow v2")
        #expect(store.get(key) == "TaskFlow v2")
    }

    @Test func removeResetsToNil() throws {
        let key = KVKey<Bool>("isActive")
        try store.set(key, value: true)
        try store.remove(key)
        #expect(store.get(key) == nil)
    }

    @Test func containsTracksExistence() throws {
        let key = KVKey<Int>("priority")
        #expect(!store.contains(key))
        try store.set(key, value: 42)
        #expect(store.contains(key))
    }

    @Test func removeAllClearsEverything() throws {
        let key1 = KVKey<Int>("a")
        let key2 = KVKey<String>("b")
        try store.set(key1, value: 1)
        try store.set(key2, value: "hello")
        try store.removeAll()
        #expect(store.get(key1) == nil)
        #expect(store.get(key2) == nil)
    }
}

Testing Streams

Wrap a store in ReactiveKVStore to get stream(). Streams emit the current value immediately, then yield on changes. Use a Task to collect values.

StreamTests.swift
@Test func streamEmitsCurrentAndChanges() async throws {
    let store = InMemoryKVStore()
    let reactive = ReactiveKVStore(store)
    let key = KVKey<Int>("taskflow.taskCount")

    var received: [Int?] = []

    let task = Task {
        for await value in reactive.stream(key) {
            received.append(value)
            if received.count == 3 { break }
        }
    }

    // Allow the stream to start
    try await Task.sleep(for: .milliseconds(50))

    try reactive.set(key, value: 10)
    try await Task.sleep(for: .milliseconds(50))

    try reactive.set(key, value: 20)

    await task.value

    #expect(received == [nil, 10, 20]) // initial nil, then two changes
}

Testing File Storage

InMemoryFileStore stores files in a dictionary keyed by path. All FileStoring operations work identically to DiskFileStore.

FileStorageTests.swift
import Testing
import ForgeFileStore

struct FileStorageTests {
    let store = InMemoryFileStore()

    @Test func saveAndLoad() async throws {
        let data = Data("hello world".utf8)
        try await store.save(data, to: "notes/test.txt")

        let loaded = try await store.load("notes/test.txt")
        #expect(String(data: loaded, encoding: .utf8) == "hello world")
    }

    @Test func existsReturnsTrueAfterSave() async throws {
        #expect(!store.exists("file.txt"))
        try await store.save(Data("x".utf8), to: "file.txt")
        #expect(store.exists("file.txt"))
    }

    @Test func deleteRemovesFile() async throws {
        try await store.save(Data("x".utf8), to: "file.txt")
        try await store.delete("file.txt")
        #expect(!store.exists("file.txt"))
    }

    @Test func loadThrowsWhenFileNotFound() async {
        await #expect(throws: FileStoreError.self) {
            try await store.load("nonexistent.txt")
        }
    }

    @Test func overwriteFalseThrowsWhenFileExists() async throws {
        try await store.save(Data("a".utf8), to: "file.txt")
        await #expect(throws: FileStoreError.self) {
            try await store.save(Data("b".utf8), to: "file.txt", overwrite: false)
        }
    }

    @Test func listFiles() async throws {
        try await store.save(Data("a".utf8), to: "docs/a.txt")
        try await store.save(Data("b".utf8), to: "docs/b.txt")
        try await store.save(Data("c".utf8), to: "other/c.txt")

        let docs = try await store.list(in: "docs")
        #expect(docs.count == 2)
    }

    @Test func deleteAllClearsEverything() async throws {
        try await store.save(Data("a".utf8), to: "file1.txt")
        try await store.save(Data("b".utf8), to: "file2.txt")
        try await store.deleteAll()

        let total = try await store.totalSize()
        #expect(total == 0)
    }
}

Testing JSON Convenience

JSONTests.swift
struct UserProfile: Codable, Equatable {
    let name: String
    let email: String
}

@Test func saveAndLoadJSON() async throws {
    let store = InMemoryFileStore()
    let profile = UserProfile(name: "Stefan", email: "s@example.com")

    try await store.saveJSON(profile, to: "profile.json")
    let loaded: UserProfile = try await store.loadJSON("profile.json")

    #expect(loaded == profile)
}

@Test func loadJSONIfExistsReturnsNil() async throws {
    let store = InMemoryFileStore()
    let result: UserProfile? = try await store.loadJSONIfExists("missing.json")
    #expect(result == nil)
}

Testing Keychain Storage

InMemoryCrypt requires no entitlements and works in all test environments including CI.

KeychainTests.swift
import Testing
import ForgeCrypt

enum TestAuthKeys {
    static let accessToken = CryptKey<String>(key: "auth.access")
    static let refreshToken = CryptKey<String>(key: "auth.refresh")
}

struct KeychainTests {
    let keychain = InMemoryCrypt()

    @Test func getReturnsNilWhenNotSet() throws {
        let token = try keychain.get(TestAuthKeys.accessToken)
        #expect(token == nil)
    }

    @Test func setAndGet() throws {
        try keychain.set("token123", for: TestAuthKeys.accessToken)
        let token = try keychain.get(TestAuthKeys.accessToken)
        #expect(token == "token123")
    }

    @Test func deleteRemovesItem() throws {
        try keychain.set("token123", for: TestAuthKeys.accessToken)
        try keychain.delete(TestAuthKeys.accessToken)
        #expect(try keychain.get(TestAuthKeys.accessToken) == nil)
    }

    @Test func containsTracksExistence() throws {
        #expect(try !keychain.contains(TestAuthKeys.accessToken))
        try keychain.set("token", for: TestAuthKeys.accessToken)
        #expect(try keychain.contains(TestAuthKeys.accessToken))
    }

    @Test func deleteAllClearsEverything() throws {
        try keychain.set("access", for: TestAuthKeys.accessToken)
        try keychain.set("refresh", for: TestAuthKeys.refreshToken)
        try keychain.deleteAll()
        #expect(try keychain.get(TestAuthKeys.accessToken) == nil)
        #expect(try keychain.get(TestAuthKeys.refreshToken) == nil)
    }
}

Testing Services End-to-End

Combine multiple in-memory stores to test complete service layers.

TaskFlowServiceTests.swift
final class TaskFlowService {
    private let preferences: KVStoring
    private let files: FileStoring
    private let keychain: CryptStoring

    init(preferences: KVStoring, files: FileStoring, keychain: CryptStoring) {
        self.preferences = preferences
        self.files = files
        self.keychain = keychain
    }

    func login(token: String, avatar: Data) async throws {
        try keychain.set(token, for: AuthKeys.accessToken)
        try await files.save(avatar, to: "avatars/current.jpg")
        try preferences.set(TaskKeys.onboarding, value: true)
    }

    func logout() async throws {
        try keychain.deleteAll()
        try await files.deleteAll()
        try preferences.removeAll()
    }
}

struct TaskFlowServiceTests {
    let prefs = InMemoryKVStore()
    let files = InMemoryFileStore()
    let keychain = InMemoryCrypt()
    let service: TaskFlowService

    init() {
        service = TaskFlowService(
            preferences: prefs,
            files: files,
            keychain: keychain
        )
    }

    @Test func loginStoresAllData() async throws {
        try await service.login(token: "abc", avatar: Data("img".utf8))

        #expect(try keychain.contains(AuthKeys.accessToken))
        #expect(files.exists("avatars/current.jpg"))
        #expect(prefs.get(TaskKeys.onboarding) == true)
    }

    @Test func logoutClearsAllData() async throws {
        try await service.login(token: "abc", avatar: Data("img".utf8))
        try await service.logout()

        #expect(try keychain.get(AuthKeys.accessToken) == nil)
        #expect(!files.exists("avatars/current.jpg"))
        #expect(prefs.get(TaskKeys.onboarding) == nil)
    }
}