Mocking a Filesystem in Jest for Unit Testing
Testing code that interacts with the filesystem can be challenging because it relies on external state, is slow, and can have side effects. This challenge asks you to create an in-memory filesystem mock that can be used within Jest tests to simulate filesystem operations, allowing for faster, more predictable, and isolated unit tests.
Problem Description
Your task is to create a TypeScript class that simulates a filesystem. This class should provide methods to mimic common filesystem operations like creating directories, writing files, reading files, and checking for the existence of files and directories. This mock filesystem will be invaluable for unit testing modules that depend on file I/O, enabling you to control the environment and ensure test determinism.
Key Requirements:
createDirectory(path: string): void: Creates a directory at the given path. If parent directories don't exist, they should be created automatically.writeFile(path: string, content: string): void: Writes content to a file at the given path. If the directory for the file doesn't exist, it should be created. If the file already exists, its content should be overwritten.readFile(path: string): string | undefined: Reads the content of a file at the given path. Returnsundefinedif the file does not exist.exists(path: string): boolean: Checks if a file or directory exists at the given path.deleteFile(path: string): void: Deletes a file at the given path. If the file doesn't exist, it should do nothing.deleteDirectory(path: string): void: Deletes a directory at the given path. The directory must be empty. If the directory doesn't exist or is not empty, it should throw an error.
Expected Behavior:
- Paths should be handled as strings, using
/as the directory separator. - The filesystem should be entirely in-memory. No actual disk I/O should occur.
- Error handling should be considered, especially for
deleteDirectory.
Edge Cases:
- Creating a directory with an empty path.
- Writing to a file in a non-existent directory.
- Reading from a non-existent file.
- Deleting a non-existent file.
- Deleting a non-empty directory.
- Attempting to create a file where a directory already exists, or vice-versa.
Examples
Example 1:
const mockFs = new InMemoryFilesystem();
mockFs.createDirectory('/home/user');
mockFs.writeFile('/home/user/document.txt', 'Hello, world!');
console.log(mockFs.readFile('/home/user/document.txt'));
// Expected Output: "Hello, world!"
console.log(mockFs.exists('/home/user'));
// Expected Output: true
console.log(mockFs.exists('/home/user/document.txt'));
// Expected Output: true
Explanation: This demonstrates basic directory creation and file writing, followed by reading and checking for existence.
Example 2:
const mockFs = new InMemoryFilesystem();
mockFs.writeFile('/app/config.json', '{ "port": 8080 }');
console.log(mockFs.readFile('/app/config.json'));
// Expected Output: '{ "port": 8080 }'
mockFs.writeFile('/app/config.json', '{ "port": 3000, "debug": true }');
console.log(mockFs.readFile('/app/config.json'));
// Expected Output: '{ "port": 3000, "debug": true }'
Explanation: Shows that writeFile correctly overwrites existing file content.
Example 3:
const mockFs = new InMemoryFilesystem();
mockFs.createDirectory('/data');
mockFs.writeFile('/data/temp.log', 'initial log');
mockFs.deleteFile('/data/temp.log');
console.log(mockFs.exists('/data/temp.log'));
// Expected Output: false
try {
mockFs.deleteDirectory('/data'); // Directory is empty, should succeed
console.log('Directory deleted successfully');
} catch (error: any) {
console.error('Error:', error.message);
}
// Expected Output: Directory deleted successfully
try {
mockFs.createDirectory('/data');
mockFs.writeFile('/data/another.txt', 'content');
mockFs.deleteDirectory('/data'); // Directory is not empty, should throw
} catch (error: any) {
console.error('Error:', error.message);
}
// Expected Output: Error: Directory '/data' is not empty.
Explanation: Demonstrates deleteFile and deleteDirectory, including the error handling for non-empty directories.
Constraints
- The filesystem should handle standard Unix-like path separators (
/). - No external libraries should be used to simulate the filesystem (e.g.,
memfsormock-fsare not allowed for the core implementation). - The solution should be written in TypeScript.
- Performance should be reasonable for typical unit testing scenarios.
Notes
Consider how you will represent the directory structure and file content in memory. A tree-like structure or a map could be suitable. Think about how to handle nested directories when creating them. For deleteDirectory, you'll need to check if it's empty before deletion.