jimfs-file-system-mocking
File System Mocking with Jimfs
1. Overview
Typically, when testing components that make heavy use of I/O operations, our tests can suffer from several issues such as poor performance, platform dependency, and unexpected state.
In this tutorial, we’ll take a look at how we can alleviate these problems using the in-memory file system Jimfs.
2. Introduction to Jimfs
and supports almost every feature of it. This is particularly useful, as it means we can emulate a virtual in-memory filesystem and interact with it using our existing java.nio layer.
As we’re going to see, it may be beneficial to use a mocked file system instead of a real one in order to:
-
avoid being dependent on the file system that is currently running the test
-
ensure the filesystem gets assembled with the expected state on each test run
-
help speed up our tests
As file systems vary considerably, using Jimfs also facilitates easily testing with file systems from different operating systems.
3. Maven Dependencies
<dependency>
<groupId>com.google.jimfs</groupId>
<artifactId>jimfs</artifactId>
<version>1.1</version>
</dependency>
4. A Simple File Repository
We’ll start by defining a simple FileRepository class that implements some standard CRUD operations:
public class FileRepository {
void create(Path path, String fileName) {
Path filePath = path.resolve(fileName);
try {
Files.createFile(filePath);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
String read(Path path) {
try {
return new String(Files.readAllBytes(path));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
String update(Path path, String newContent) {
try {
Files.write(path, newContent.getBytes());
return newContent;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
void delete(Path path) {
try {
Files.deleteIfExists(path);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
As we can see, each method is making use of standard java.nio classes.
4.1. Creating a File
@Test
@DisplayName("Should create a file on a file system")
void givenUnixSystem_whenCreatingFile_thenCreatedInPath() {
FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());
String fileName = "newFile.txt";
Path pathToStore = fileSystem.getPath("");
fileRepository.create(pathToStore, fileName);
assertTrue(Files.exists(pathToStore.resolve(fileName)));
}
In this example, we’ve used the static method Jimfs.newFileSystem() to create a new in-memory file system. We pass a configuration object Configuration.unix(), which creates an immutable configuration for a Unix file system. This includes important OS-specific information such as path separators and information about symbolic links.
Now that we’ve created a file, we’re able to check if the file was created successfully on the Unix-based system.
4.2. Reading a File
@Test
@DisplayName("Should read the content of the file")
void givenOSXSystem_whenReadingFile_thenContentIsReturned() throws Exception {
FileSystem fileSystem = Jimfs.newFileSystem(Configuration.osX());
Path resourceFilePath = fileSystem.getPath(RESOURCE_FILE_NAME);
Files.copy(getResourceFilePath(), resourceFilePath);
String content = fileRepository.read(resourceFilePath);
assertEquals(FILE_CONTENT, content);
}
This time around, we’ve checked if it’s possible to read the content of the file on a macOS (formerly OSX) system by simply using a different type of configuration — Jimfs.newFileSystem(Configuration.osX()).
4.3. Updating a File
@Test
@DisplayName("Should update the content of the file")
void givenWindowsSystem_whenUpdatingFile_thenContentHasChanged() throws Exception {
FileSystem fileSystem = Jimfs.newFileSystem(Configuration.windows());
Path resourceFilePath = fileSystem.getPath(RESOURCE_FILE_NAME);
Files.copy(getResourceFilePath(), resourceFilePath);
String newContent = "I'm updating you.";
String content = fileRepository.update(resourceFilePath, newContent);
assertEquals(newContent, content);
assertEquals(newContent, fileRepository.read(resourceFilePath));
}
Likewise, this time we’ve checked how the method behaves on a Windows-based system by using *Jimfs.newFileSystem(Configuration.windows())*.
4.4. Deleting a File
@Test
@DisplayName("Should delete file")
void givenCurrentSystem_whenDeletingFile_thenFileHasBeenDeleted() throws Exception {
FileSystem fileSystem = Jimfs.newFileSystem();
Path resourceFilePath = fileSystem.getPath(RESOURCE_FILE_NAME);
Files.copy(getResourceFilePath(), resourceFilePath);
fileRepository.delete(resourceFilePath);
assertFalse(Files.exists(resourceFilePath));
}
Unlike previous examples, we’ve used Jimfs.newFileSystem() without specifying a file system configuration. In this case, Jimfs will create a new in-memory file system with a default configuration appropriate to the current operating system.
5. Moving a File
Firstly, let’s implement the move method using the standard java.nio.file.File class:
void move(Path origin, Path destination) {
try {
Files.createDirectories(destination);
Files.move(origin, destination, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
We’re going to use a parameterized test to ensure that this method works on several different file systems:
private static Stream<Arguments> provideFileSystem() {
return Stream.of(
Arguments.of(Jimfs.newFileSystem(Configuration.unix())),
Arguments.of(Jimfs.newFileSystem(Configuration.windows())),
Arguments.of(Jimfs.newFileSystem(Configuration.osX())));
}
@ParameterizedTest
@DisplayName("Should move file to new destination")
@MethodSource("provideFileSystem")
void givenEachSystem_whenMovingFile_thenMovedToNewPath(FileSystem fileSystem) throws Exception {
Path origin = fileSystem.getPath(RESOURCE_FILE_NAME);
Files.copy(getResourceFilePath(), origin);
Path destination = fileSystem.getPath("newDirectory", RESOURCE_FILE_NAME);
fileManipulation.move(origin, destination);
assertFalse(Files.exists(origin));
assertTrue(Files.exists(destination));
}
As we can see, we’ve also been able to use Jimfs to test that we can move files on a variety of different file systems from a single unit test.
6. Operating System Dependent Tests
To demonstrate another benefit of using Jimfs, let’s create a FilePathReader class. The class is responsible for returning the real system path, which is, of course, OS-dependent:
class FilePathReader {
String getSystemPath(Path path) {
try {
return path
.toRealPath()
.toString();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
Now, let’s add a test for this class:
class FilePathReaderUnitTest {
private static String DIRECTORY_NAME = "baeldung";
private FilePathReader filePathReader = new FilePathReader();
@Test
@DisplayName("Should get path on windows")
void givenWindowsSystem_shouldGetPath_thenReturnWindowsPath() throws Exception {
FileSystem fileSystem = Jimfs.newFileSystem(Configuration.windows());
Path path = getPathToFile(fileSystem);
String stringPath = filePathReader.getSystemPath(path);
assertEquals("C:\\work\\" + DIRECTORY_NAME, stringPath);
}
@Test
@DisplayName("Should get path on unix")
void givenUnixSystem_shouldGetPath_thenReturnUnixPath() throws Exception {
FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());
Path path = getPathToFile(fileSystem);
String stringPath = filePathReader.getSystemPath(path);
assertEquals("/work/" + DIRECTORY_NAME, stringPath);
}
private Path getPathToFile(FileSystem fileSystem) throws Exception {
Path path = fileSystem.getPath(DIRECTORY_NAME);
Files.createDirectory(path);
return path;
}
}
As we can see, the output for Windows differs from the one of Unix, as we’d expect. Moreover, we didn’t have to run these tests using two different file systems — Jimfs mocked it for us automatically.
It’s worth mentioning that Jimfs doesn’t support the toFile() method that returns a java.io.File. It’s the only method from the Path class that isn’t supported. Therefore, it might be better to operate on an InputStream rather than a File.
7. Conclusion
In this article, we’ve learned how to use use the in-memory file system Jimfs to mock file system interactions from our unit tests.
First, we started by defining a simple file repository with several CRUD operations. Then we saw examples of how to test each of the methods using a different file system type. Finally, we saw an example of how we can use Jimfs to test OS-dependent file system handling.
As always, the code for these examples is available over on Github.