Programming > Java > java > nio > file
Jul 16, 2025
Jul 16, 2025
N/A Views
MD

Warning

This article was automatically translated by OpenAI (gpt-4.1).It may be edited eventually, but please be aware that it may contain incorrect information at this time.

When creating NIO.2 Path objects in Java, it's common to use Path.of() or Paths.get(), but from a testability perspective, it's better to use FileSystem#getPath().

Problems That Can Occur When Using Path.of

When you use Path.of() or Paths.get(), you depend on the actual file system, which can cause the following issues during testing:

  • Differences in path separators depending on the OS
  • Permission issues
  • Potential dependency on the state of the test environment

Let's look at a simple example:

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class FileService {

    public void writeFile(String filename, List<String> lines) throws IOException {
        Path path = Path.of(filename);
        Files.write(path, lines);
    }

    public List<String> readFile(String filename) throws IOException {
        Path path = Path.of(filename);
        return Files.readAllLines(path);
    }

}

Note

Strictly speaking, it's better to receive filename as a Path rather than a String, but for the purpose of this article, we're using String.

Let's try testing this FileService. The following test code looks like it should work correctly:

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class FileServiceTest {

    @Test
    void testWriteAndReadFile() throws Exception {
        FileService fileService = new FileService();
        List<String> lines = List.of("line1", "line2", "line3");

        Path dir = Path.of("/test");
        Files.createDirectories(dir);
        fileService.writeFile(dir.resolve("data.txt").toString(), lines);

        List<String> readLines = fileService.readFile(dir.resolve("data.txt").toString());
        assertThat(readLines).containsExactlyElementsOf(lines);
    }

}

However, on my Mac, the following exception actually occurs:

java.nio.file.FileSystemException: /test: Read-only file system

Other issues may occur on Windows.

Many people probably write tests using temporary files with @TempDir, like this:

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.assertj.core.api.Assertions.assertThat;

class FileServiceTest {

    @Test
    void testWriteAndReadFileWithTempDir(@TempDir Path tempDir) throws Exception {
        FileService fileService = new FileService();
        List<String> lines = List.of("line1", "line2", "line3");

        Path dir = tempDir.resolve("test");
        Files.createDirectories(dir);
        fileService.writeFile(dir.resolve("data.txt").toString(), lines);

        List<String> readLines = fileService.readFile(dir.resolve("data.txt").toString());
        assertThat(readLines).containsExactlyElementsOf(lines);
    }

}

Even if the test seems to work at first glance, if you're not careful with your code, the test may fail because path separators differ between Windows and Unix-like systems.
For example, if you run a test for a batch process that actually runs on Linux, but you run the test on Windows, the test may fail due to different path separators.
If some developers on the same project use Windows and others use Mac, the test may pass in one person's environment but fail in another's.

Implementation Using FileSystem#getPath

Path.of(String) internally uses the getPath method of the default FileSystem (FileSystems.getDefault()). The implementation looks like this:

    public static Path of(String first, String... more) {
        return FileSystems.getDefault().getPath(first, more);
    }

FileSystem is an abstraction of the file system built into the JDK, and you can use an in-memory file system during testing.
Implementations of FileSystem can be extended via the java.nio.file.spi.FileSystemProvider SPI, and there are implementations for S3 and Azure Blob Storage, for example.

By designing your code to receive a FileSystem via constructor injection, you can inject any FileSystem during testing. Instead of creating a Path with Path#of, use FileSystem#getPath:

import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class FileService {

    private final FileSystem fileSystem;

    public FileService(FileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    public void writeFile(String filename, List<String> lines) throws IOException {
        Path path = fileSystem.getPath(filename);
        Files.write(path, lines);
    }

    public List<String> readFile(String filename) throws IOException {
        Path path = fileSystem.getPath(filename);
        return Files.readAllLines(path);
    }

}

In production code, use the default FileSystem. In Spring, you can define a bean like this:

import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
class AppConfig {

    @Bean
    FileSystem fileSystem() {
        return FileSystems.getDefault();
    }

}

This way, you can achieve essentially the same behavior as using Path.of(String) in FileService, while having the flexibility to inject a different FileSystem implementation during testing.

Note

Another way to abstract Path is to use Path.of(URI), such as Path.of(URI.create("file:///test/data.txt")), specifying the URI scheme.
In this case, the FileSystem corresponding to the scheme is used. You can add new schemes by implementing the SPI java.nio.file.spi.FileSystemProvider and registering it, which will be loaded via ServiceLoader.
However, Jimfs, which will be explained next, does not support URI schemes via SPI, and with dependency injection, it's easier to switch the FileSystem than to switch the string for Path. That's why this article introduces the method using FileSystem#getPath().

Testing with Jimfs

Jimfs is an in-memory file system implementation open-sourced by Google.
By using this, you can run tests without depending on the actual file system.

Add the following dependency to your pom.xml to use Jimfs in your tests:

<dependency>
    <groupId>com.google.jimfs</groupId>
    <artifactId>jimfs</artifactId>
    <version>1.3.1</version>
    <scope>test</scope>
</dependency>

If you rewrite the previous test using Jimfs, it looks like this:

import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class FileServiceTest {

    @Test
    void testWriteAndReadFile() throws Exception {
        try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix())) {
            FileService fileService = new FileService(fileSystem);
            List<String> lines = List.of("line1", "line2", "line3");

            Path dir = fileSystem.getPath("/test");
            Files.createDirectories(dir);
            fileService.writeFile(dir.resolve("data.txt").toString(), lines);

            List<String> readLines = fileService.readFile(dir.resolve("data.txt").toString());
            assertThat(readLines).containsExactlyElementsOf(lines);
        }
    }

}

Since the FileSystem is created in memory, you can run tests without worrying about differences between environments.
Also, you can transparently switch the FileSystem without making any changes to your production code.


By designing your code to create Path objects using FileSystem#getPath(), you can inject in-memory file systems like Jimfs during testing, enabling faster and more stable tests.
This method is especially effective for applications with a lot of file I/O or those that need to support multiple platforms.

Found a mistake? Update the entry.
Share this article: