Testing Mastery

In-Memory File Systems for Tests

April 4, 2026
5 min read
Featured image for blog post: In-Memory File Systems for Tests

Now let's build the in-memory file system that makes your cache tests fast and deterministic.

Follow along with the code: iOS-Practice on GitHub

The InMemoryFileSystem

class InMemoryFileSystem: FileSystemProtocol {
    private var files: [String: Data] = [:]
    private var directories: Set<String> = []
    var writeError: Error?
    var readError: Error?

    func fileExists(atPath path: String) -> Bool {
        files[path] != nil
    }

    func createDirectory(at url: URL, withIntermediateDirectories: Bool) throws {
        directories.insert(url.path)
    }

    func write(_ data: Data, to url: URL) throws {
        if let error = writeError { throw error }
        files[url.path] = data
    }

    func read(from url: URL) throws -> Data {
        if let error = readError { throw error }
        guard let data = files[url.path] else {
            throw NSError(domain: "FileNotFound", code: 404)
        }
        return data
    }

    func removeItem(at url: URL) throws {
        files.removeValue(forKey: url.path)
    }

    func contentsOfDirectory(at url: URL) throws -> [URL] {
        files.keys
            .filter { $0.hasPrefix(url.path) }
            .map { URL(fileURLWithPath: $0) }
    }

    func reset() {
        files.removeAll()
        directories.removeAll()
        writeError = nil
        readError = nil
    }
}

Fast, Isolated Tests

class ImageCacheManagerTests: XCTestCase {
    var fileSystem: InMemoryFileSystem!
    var sut: ImageCacheManager!
    let testDirectory = URL(fileURLWithPath: "/test/cache")

    override func setUp() {
        fileSystem = InMemoryFileSystem()
        sut = ImageCacheManager(fileSystem: fileSystem, cacheDirectory: testDirectory)
    }

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

    func test_cacheImage_storesDataInFileSystem() throws {
        let imageData = Data([0x89, 0x50, 0x4E, 0x47]) // PNG header

        try sut.cacheImage(id: "test_image", data: imageData)

        let expectedPath = testDirectory.appendingPathComponent("test_image.cache").path
        XCTAssertTrue(fileSystem.fileExists(atPath: expectedPath))
    }

    func test_getCachedImage_returnsStoredImage() throws {
        let imageData = Data([0x89, 0x50, 0x4E, 0x47])
        try sut.cacheImage(id: "test_image", data: imageData)

        let cached = sut.getCachedImage(id: "test_image")

        XCTAssertNotNil(cached)
        XCTAssertEqual(cached?.id, "test_image")
        XCTAssertEqual(cached?.data, imageData)
    }

    func test_getCachedImage_whenNotCached_returnsNil() {
        let cached = sut.getCachedImage(id: "nonexistent")
        XCTAssertNil(cached)
    }
}

Testing Error Scenarios

func test_cacheImage_whenWriteFails_throwsError() {
    fileSystem.writeError = NSError(domain: "DiskFull", code: 507)

    XCTAssertThrowsError(try sut.cacheImage(id: "test", data: Data()))
}

func test_deleteCachedImage_removesFromFileSystem() throws {
    try sut.cacheImage(id: "test_image", data: Data())

    try sut.deleteCachedImage(id: "test_image")

    XCTAssertNil(sut.getCachedImage(id: "test_image"))
}

Benefits

Before (FileManager.default)After (InMemoryFileSystem)
Creates real files on diskEverything stays in memory
Slow disk I/OInstant operations
Cleanup requiredReset in tearDown
CI environment issuesWorks anywhere
Tests can interfereComplete isolation

The in-memory file system runs in microseconds instead of milliseconds, and you never worry about leftover test files.

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