Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

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

First of all, let’s add the project dependencies we'll need for our examples:

<dependency>
    <groupId>com.google.jimfs</groupId>
    <artifactId>jimfs</artifactId>
    <version>1.1</version>
</dependency>

The jimfs dependency contains everything that we need in order to use our mocked file system. Additionally, we'll be writing tests using JUnit5.

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

In this section, we'll write a test that tests the create method from our repository:

@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

Next, we'll test the method that reads the content of the 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

We can also use Jimfs to test the method that updates the content of the 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

To conclude testing our CRUD operations, let's test the method that deletes the 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

In this section, we'll learn how to test a method that moves a file from one directory to another.

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.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

Leave a Reply

avatar
  Subscribe  
Notify of