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.
// 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()
}
} // 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
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.
@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.
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
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.
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.
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)
}
}