---
title: How to Create Highly Testable java.nio.file.Path Objects
tags: ["Java", "Testing", "Jimfs", "JUnit"]
categories: ["Programming", "Java", "java", "nio", "file"]
date: 2025-07-16T01:42:20Z
updated: 2025-07-16T02:08:15Z
---

> ⚠️ 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:

```java
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:

```java
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:

```java
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:

```java
    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`](https://docs.oracle.com/javase/7/docs/api/java/nio/file/spi/FileSystemProvider.html) SPI, and there are implementations for [S3](https://aws.amazon.com/jp/blogs/storage/extending-java-applications-to-directly-access-files-in-amazon-s3-without-recompiling/) and [Azure Blob Storage](https://learn.microsoft.com/en-us/java/api/overview/azure/storage-blob-nio-readme), 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`:

```java
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:

```java
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](https://github.com/google/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:
```xml
<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:

```java
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.
