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 disk | Everything stays in memory |
| Slow disk I/O | Instant operations |
| Cleanup required | Reset in tearDown |
| CI environment issues | Works anywhere |
| Tests can interfere | Complete isolation |
The in-memory file system runs in microseconds instead of milliseconds, and you never worry about leftover test files.