Testing Mastery

InMemoryKeyValueStore Pattern

March 29, 2026
5 min read
Featured image for blog post: InMemoryKeyValueStore Pattern

An in-memory store gives you complete isolation between tests—no cleanup required.

Follow along with the code: iOS-Practice on GitHub

The InMemoryKeyValueStore

class InMemoryKeyValueStore: KeyValueStore {
    private var storage: [String: Any] = [:]

    func bool(forKey key: String) -> Bool {
        storage[key] as? Bool ?? false
    }

    func string(forKey key: String) -> String? {
        storage[key] as? String
    }

    func integer(forKey key: String) -> Int {
        storage[key] as? Int ?? 0
    }

    func setBool(_ value: Bool, forKey key: String) {
        storage[key] = value
    }

    func setString(_ value: String?, forKey key: String) {
        if let value = value {
            storage[key] = value
        } else {
            storage.removeValue(forKey: key)
        }
    }

    func setInt(_ value: Int, forKey key: String) {
        storage[key] = value
    }

    func removeObject(forKey key: String) {
        storage.removeValue(forKey: key)
    }

    // Helper for tests
    func reset() {
        storage.removeAll()
    }
}

Clean, Isolated Tests

class OnboardingManagerTests: XCTestCase {
    var store: InMemoryKeyValueStore!
    var sut: OnboardingManager!

    override func setUp() {
        store = InMemoryKeyValueStore()
        sut = OnboardingManager(store: store)
    }

    override func tearDown() {
        store.reset()
    }

    func test_shouldShowOnboarding_onFirstLaunch_returnsTrue() {
        // Fresh store = first launch
        XCTAssertTrue(sut.shouldShowOnboarding())
    }

    func test_shouldShowOnboarding_afterCompletion_returnsFalse() {
        sut.completeOnboarding()
        XCTAssertFalse(sut.shouldShowOnboarding())
    }
}

Testing Version Upgrades

func test_shouldShowWhatsNew_onFirstInstall_returnsFalse() {
    // No previous version = first install, show onboarding instead
    XCTAssertFalse(sut.shouldShowWhatsNew(currentVersion: "1.0.0"))
}

func test_shouldShowWhatsNew_whenVersionChanged_returnsTrue() {
    sut.updateVersion(to: "1.0.0")
    XCTAssertTrue(sut.shouldShowWhatsNew(currentVersion: "2.0.0"))
}

func test_shouldShowWhatsNew_whenVersionSame_returnsFalse() {
    sut.updateVersion(to: "1.0.0")
    XCTAssertFalse(sut.shouldShowWhatsNew(currentVersion: "1.0.0"))
}

Testing Launch Count Logic

func test_shouldShowRatePrompt_atLaunchCount10_returnsTrue() {
    store.setInt(10, forKey: "launchCount")
    XCTAssertTrue(sut.shouldShowRatePrompt())
}

func test_shouldShowRatePrompt_atLaunchCount5_returnsFalse() {
    // 5 % 10 != 0, so no prompt
    store.setInt(5, forKey: "launchCount")
    XCTAssertFalse(sut.shouldShowRatePrompt())
}

func test_incrementLaunchCount_incrementsFromZero() {
    sut.incrementLaunchCount()
    XCTAssertEqual(sut.launchCount, 1)
}

Benefits

Before (UserDefaults.standard)After (InMemoryKeyValueStore)
Tests pollute each otherEach test gets fresh state
Tests affect real app dataCompletely isolated
Need manual cleanupReset in tearDown
Order-dependent failuresRun in any order
Can't test "first launch"Always starts empty

The in-memory store is fast, isolated, and requires no cleanup between test runs.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch