Hone logo
Hone
Problems

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. Returns undefined if 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., memfs or mock-fs are 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.

Loading editor...
typescript