Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.
File System Testing: Using System.IO.Abstractions to Simulate File Operations
Applicable Scenarios
Use this skill when asked to perform the following tasks:
- Refactor code directly using
System.IO.File, System.IO.Directory and other static classes
- Write unit tests for code involving file read/write, directory operations
- Use MockFileSystem to simulate various file system states
- Test exception scenarios like insufficient file permissions, file not found
- Design testable file processing service architecture
Core Principles
1. Fundamental Problem of File System Dependencies
Traditional code directly using System.IO static classes is difficult to test, reasons include:
- Speed Issues: Actual disk IO is 10-100x slower than memory operations
- Environment Dependency: Test results affected by file system state, permissions, paths
- Side Effects: Tests leave traces on disk, affecting other tests
- Concurrency Issues: Multiple tests operating on same file create race conditions
- Error Simulation Difficulty: Difficult to simulate insufficient permissions, insufficient disk space, etc.
2. System.IO.Abstractions Solution
This is a package that wraps System.IO static classes into interfaces, supporting dependency injection and test doubles.
Core Interface Architecture:
public interface IFileSystem
{
IFile File { get; }
IDirectory Directory { get; }
IFileInfo FileInfo { get; }
IDirectoryInfo DirectoryInfo { get; }
IPath Path { get; }
IDriveInfo DriveInfo { get; }
}
```text
**Required NuGet Packages**:
```xml
<!-- Production environment -->
<PackageReference Include="System.IO.Abstractions" Version="21.*" />
<!-- Test project -->
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.*" />
```text
### 3. Refactoring Steps
**Step 1**: Change code directly using static classes to depend on `IFileSystem`
```csharp
// ❌ Before refactoring (not testable)
public class ConfigService
{
public string LoadConfig(string path)
{
return File.ReadAllText(path);
}
}
// ✅ After refactoring (testable)
public class ConfigService
{
private readonly IFileSystem _fileSystem;
public ConfigService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public string LoadConfig(string path)
{
return _fileSystem.File.ReadAllText(path);
}
}
```text
**Step 2**: Register real implementation in DI container
```csharp
// Program.cs
services.AddSingleton<IFileSystem, FileSystem>();
services.AddScoped<ConfigService>();
```text
**Step 3**: Use MockFileSystem in tests
```csharp
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
["config.json"] = new MockFileData("{ \"key\": \"value\" }")
});
var service = new ConfigService(mockFs);
```text
## MockFileSystem Testing Patterns
### Pattern 1: Default File State
```csharp
[Fact]
public async Task LoadConfig_File_Exists_Should_Return_Content()
{
// Arrange - Create default file system state
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
["config.json"] = new MockFileData("{ \"key\": \"value\" }"),
[@"C:\data\users.csv"] = new MockFileData("Name,Age\nJohn,25"),
[@"C:\logs\"] = new MockDirectoryData() // Empty directory
});
var service = new ConfigService(mockFileSystem);
// Act
var result = await service.LoadConfigAsync("config.json");
// Assert
result.Should().Contain("key");
}
```text
### Pattern 2: Verify Write Results
```csharp
[Fact]
public async Task SaveConfig_Specified_Content_Should_Write_Correctly()
{
// Arrange
var mockFileSystem = new MockFileSystem();
var service = new ConfigService(mockFileSystem);
// Act
await service.SaveConfigAsync("output.json", "{ \"saved\": true }");
// Assert - Verify final state of file system
mockFileSystem.File.Exists("output.json").Should().BeTrue();
var content = await mockFileSystem.File.ReadAllTextAsync("output.json");
content.Should().Contain("saved");
}
```text
### Pattern 3: Test Directory Operations
```csharp
[Fact]
public void CopyFile_Target_Directory_Not_Exists_Should_Auto_Create()
{
// Arrange
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
[@"C:\source\file.txt"] = new MockFileData("content")
});
var service = new FileManagerService(mockFileSystem);
// Act
service.CopyFileToDirectory(@"C:\source\file.txt", @"C:\target\subfolder");
// Assert
mockFileSystem.Directory.Exists(@"C:\target\subfolder").Should().BeTrue();
mockFileSystem.File.Exists(@"C:\target\subfolder\file.txt").Should().BeTrue();
}
```text
### Pattern 4: Use NSubstitute to Simulate Errors
When needing to simulate specific exceptions, MockFileSystem has limited support, can use NSubstitute:
```csharp
[Fact]
public void TryReadFile_Insufficient_Permissions_Should_Return_False()
{
// Arrange
var mockFileSystem = Substitute.For<IFileSystem>();
var mockFile = Substitute.For<IFile>();
mockFileSystem.File.Returns(mockFile);
mockFile.Exists("protected.txt").Returns(true);
mockFile.ReadAllText("protected.txt")
.Throws(new UnauthorizedAccessException("Access denied"));
var service = new FilePermissionService(mockFileSystem);
// Act
var result = service.TryReadFile("protected.txt", out var content);
// Assert
result.Should().BeFalse();
content.Should().BeNull();
}
```text
## Advanced Testing Techniques
### Stream Operation Testing
```csharp
[Fact]
public async Task CountLines_Multi_Line_File_Should_Return_Correct_Count()
{
// Arrange
var content = "Line 1\nLine 2\nLine 3\nLine 4";
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
["data.txt"] = new MockFileData(content)
});
var processor = new StreamProcessorService(mockFileSystem);
// Act
var result = await processor.CountLinesAsync("data.txt");
// Assert
result.Should().Be(4);
}
```text
### File Information Testing
```csharp
[Fact]
public void GetFileInfo_File_Exists_Should_Return_Correct_Info()
{
// Arrange
var content = "Hello, World!";
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
[@"C:\test.txt"] = new MockFileData(content)
});
var service = new FileManagerService(mockFileSystem);
// Act
var info = service.GetFileInfo(@"C:\test.txt");
// Assert
info.Should().NotBeNull();
info!.Name.Should().Be("test.txt");
info.Size.Should().Be(content.Length);
}
```text
### Backup File Testing
```csharp
[Fact]
public void BackupFile_File_Exists_Should_Create_Timestamp_Backup()
{
// Arrange
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
[@"C:\data\important.txt"] = new MockFileData("important data")
});
var service = new FileManagerService(mockFileSystem);
// Act
var backupPath = service.BackupFile(@"C:\data\important.txt");
// Assert
backupPath.Should().StartWith(@"C:\data\important_");
backupPath.Should().EndWith(".txt");
mockFileSystem.File.Exists(backupPath).Should().BeTrue();
}
```text
## Best Practices
### ✅ Should Do
1. **Use Path.Combine to handle paths**:
```csharp
var path = _fileSystem.Path.Combine("configs", "app.json");
```text
2. **Defensively check file existence**:
```csharp
if (!_fileSystem.File.Exists(filePath))
{
return defaultValue;
}
```text
3. **Auto-create necessary directories**:
```csharp
var dir = _fileSystem.Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir) && !_fileSystem.Directory.Exists(dir))
{
_fileSystem.Directory.CreateDirectory(dir);
}
```text
4. **Properly handle various IO exceptions**:
```csharp
try
{
return await _fileSystem.File.ReadAllTextAsync(path);
}
catch (UnauthorizedAccessException) { /* insufficient permissions */ }
catch (IOException) { /* file locked */ }
catch (DirectoryNotFoundException) { /* directory not found */ }
```text
5. **Use independent MockFileSystem for each test**:
```csharp
public class ServiceTests
{
[Fact]
public void Test1()
{
var mockFs = new MockFileSystem(); // Independent instance
}
[Fact]
public void Test2()
{
var mockFs = new MockFileSystem(); // Independent instance
}
}
```text
### ❌ Should Avoid
1. **Hardcode path separators**:
```csharp
// ❌ Don't do this
var path = "configs\\app.json"; // Windows only
var path = "configs/app.json"; // Unix only
// ✅ Should do this
var path = _fileSystem.Path.Combine("configs", "app.json");
```text
2. **Use real file system in unit tests**:
```csharp
// ❌ This is not a unit test
var realFs = new FileSystem();
// ✅ Unit tests should use MockFileSystem
var mockFs = new MockFileSystem();
```text
3. **Ignore exception handling**:
```csharp
// ❌ Don't assume file always exists
var content = _fileSystem.File.ReadAllText(path);
// ✅ Add existence check and exception handling
if (_fileSystem.File.Exists(path))
{
try { return _fileSystem.File.ReadAllText(path); }
catch (IOException) { return defaultValue; }
}
```text
## Performance Considerations
### MockFileSystem Advantages
- **Speed**: 10-100x faster than real file operations
- **Reliability**: Not affected by disk state
- **Isolation**: Complete isolation between tests
- **Error Simulation**: Can precisely simulate various exception scenarios
### Memory Usage Recommendations
- Only create files necessary for testing
- Avoid simulating oversized files in tests
- For large file processing logic, use moderately sized test data:
```csharp
// ✅ Moderately sized test data
var testContent = string.Join("\n",
Enumerable.Range(1, 1000).Select(i => $"Line {i}"));
mockFileSystem.AddFile("test.txt", new MockFileData(testContent));
```text
## Practical Integration Examples
### Configuration File Management Service
See `templates/configmanager-service.cs` for complete implementation, including:
- Configuration file load and save
- JSON serialization and deserialization
- Auto-create directories
- Configuration file backup functionality
### File Management Service
See `templates/filemanager-service.cs` for implementation, including:
- File copy and backup
- Directory operations
- File information query
- Error handling patterns
## Reference Resources
### Original Articles
This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:
- **Day 17 - File and IO Testing: Using System.IO.Abstractions to Simulate File System**
- Article: https://ithelp.ithome.com.tw/articles/10375981
- Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day17
### Official Documentation
- [System.IO.Abstractions GitHub](https://github.com/TestableIO/System.IO.Abstractions)
- [System.IO.Abstractions NuGet](https://www.nuget.org/packages/System.IO.Abstractions/)
- [TestingHelpers NuGet](https://www.nuget.org/packages/System.IO.Abstractions.TestingHelpers/)
### Related Skills
- `nsubstitute-mocking` - Test doubles and mocking
- `unit-test-fundamentals` - Unit testing basics
1---2name: dotnet-testing-filesystem-testing-abstractions3category: testing4subcategory: specialized5description: |6 Specialized skill for testing file system operations using System.IO.Abstractions. Use when you need to test File, Directory, Path operations, or simulate file system. Covers IFileSystem, MockFileSystem, file read/write testing, directory operation testing, etc.7 Keywords: file testing, filesystem, file testing, file system testing, IFileSystem, MockFileSystem, System.IO.Abstractions, File.ReadAllText, File.WriteAllText, Directory.CreateDirectory, Path.Combine, mock file system, file abstraction8targets: ['*']9license: MIT10metadata:11 author: Kevin Tseng12 version: '1.0.0'13 tags: '.NET, testing, IFileSystem, MockFileSystem, file testing'14 related_skills: 'nsubstitute-mocking, unit-test-fundamentals, datetime-testing-timeprovider'15claudecode: {}16opencode: {}17codexcli:18 short-description: '.NET skill guidance for dotnet-testing-filesystem-testing-abstractions'19copilot: {}20geminicli: {}21antigravity: {}22---2324Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.2526# File System Testing: Using System.IO.Abstractions to Simulate File Operations2728## Applicable Scenarios2930Use this skill when asked to perform the following tasks:3132- Refactor code directly using `System.IO.File`, `System.IO.Directory` and other static classes33- Write unit tests for code involving file read/write, directory operations34- Use MockFileSystem to simulate various file system states35- Test exception scenarios like insufficient file permissions, file not found36- Design testable file processing service architecture3738## Core Principles3940### 1. Fundamental Problem of File System Dependencies4142Traditional code directly using `System.IO` static classes is difficult to test, reasons include:4344- **Speed Issues**: Actual disk IO is 10-100x slower than memory operations45- **Environment Dependency**: Test results affected by file system state, permissions, paths46- **Side Effects**: Tests leave traces on disk, affecting other tests47- **Concurrency Issues**: Multiple tests operating on same file create race conditions48- **Error Simulation Difficulty**: Difficult to simulate insufficient permissions, insufficient disk space, etc.4950### 2. System.IO.Abstractions Solution5152This is a package that wraps System.IO static classes into interfaces, supporting dependency injection and test doubles.5354**Core Interface Architecture**:5556````csharp57public interface IFileSystem58{59 IFile File { get; }60 IDirectory Directory { get; }61 IFileInfo FileInfo { get; }62 IDirectoryInfo DirectoryInfo { get; }63 IPath Path { get; }64 IDriveInfo DriveInfo { get; }65}66```text6768**Required NuGet Packages**:6970```xml71<!-- Production environment -->72<PackageReference Include="System.IO.Abstractions" Version="21.*" />7374<!-- Test project -->75<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.*" />76```text7778### 3. Refactoring Steps7980**Step 1**: Change code directly using static classes to depend on `IFileSystem`8182```csharp83// ❌ Before refactoring (not testable)84public class ConfigService85{86 public string LoadConfig(string path)87 {88 return File.ReadAllText(path);89 }90}9192// ✅ After refactoring (testable)93public class ConfigService94{95 private readonly IFileSystem _fileSystem;9697 public ConfigService(IFileSystem fileSystem)98 {99 _fileSystem = fileSystem;100 }101102 public string LoadConfig(string path)103 {104 return _fileSystem.File.ReadAllText(path);105 }106}107```text108109**Step 2**: Register real implementation in DI container110111```csharp112// Program.cs113services.AddSingleton<IFileSystem, FileSystem>();114services.AddScoped<ConfigService>();115```text116117**Step 3**: Use MockFileSystem in tests118119```csharp120var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>121{122 ["config.json"] = new MockFileData("{ \"key\": \"value\" }")123});124var service = new ConfigService(mockFs);125```text126127## MockFileSystem Testing Patterns128129### Pattern 1: Default File State130131```csharp132[Fact]133public async Task LoadConfig_File_Exists_Should_Return_Content()134{135 // Arrange - Create default file system state136 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>137 {138 ["config.json"] = new MockFileData("{ \"key\": \"value\" }"),139 [@"C:\data\users.csv"] = new MockFileData("Name,Age\nJohn,25"),140 [@"C:\logs\"] = new MockDirectoryData() // Empty directory141 });142143 var service = new ConfigService(mockFileSystem);144145 // Act146 var result = await service.LoadConfigAsync("config.json");147148 // Assert149 result.Should().Contain("key");150}151```text152153### Pattern 2: Verify Write Results154155```csharp156[Fact]157public async Task SaveConfig_Specified_Content_Should_Write_Correctly()158{159 // Arrange160 var mockFileSystem = new MockFileSystem();161 var service = new ConfigService(mockFileSystem);162163 // Act164 await service.SaveConfigAsync("output.json", "{ \"saved\": true }");165166 // Assert - Verify final state of file system167 mockFileSystem.File.Exists("output.json").Should().BeTrue();168 var content = await mockFileSystem.File.ReadAllTextAsync("output.json");169 content.Should().Contain("saved");170}171```text172173### Pattern 3: Test Directory Operations174175```csharp176[Fact]177public void CopyFile_Target_Directory_Not_Exists_Should_Auto_Create()178{179 // Arrange180 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>181 {182 [@"C:\source\file.txt"] = new MockFileData("content")183 });184 var service = new FileManagerService(mockFileSystem);185186 // Act187 service.CopyFileToDirectory(@"C:\source\file.txt", @"C:\target\subfolder");188189 // Assert190 mockFileSystem.Directory.Exists(@"C:\target\subfolder").Should().BeTrue();191 mockFileSystem.File.Exists(@"C:\target\subfolder\file.txt").Should().BeTrue();192}193```text194195### Pattern 4: Use NSubstitute to Simulate Errors196197When needing to simulate specific exceptions, MockFileSystem has limited support, can use NSubstitute:198199```csharp200[Fact]201public void TryReadFile_Insufficient_Permissions_Should_Return_False()202{203 // Arrange204 var mockFileSystem = Substitute.For<IFileSystem>();205 var mockFile = Substitute.For<IFile>();206207 mockFileSystem.File.Returns(mockFile);208 mockFile.Exists("protected.txt").Returns(true);209 mockFile.ReadAllText("protected.txt")210 .Throws(new UnauthorizedAccessException("Access denied"));211212 var service = new FilePermissionService(mockFileSystem);213214 // Act215 var result = service.TryReadFile("protected.txt", out var content);216217 // Assert218 result.Should().BeFalse();219 content.Should().BeNull();220}221```text222223## Advanced Testing Techniques224225### Stream Operation Testing226227```csharp228[Fact]229public async Task CountLines_Multi_Line_File_Should_Return_Correct_Count()230{231 // Arrange232 var content = "Line 1\nLine 2\nLine 3\nLine 4";233 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>234 {235 ["data.txt"] = new MockFileData(content)236 });237238 var processor = new StreamProcessorService(mockFileSystem);239240 // Act241 var result = await processor.CountLinesAsync("data.txt");242243 // Assert244 result.Should().Be(4);245}246```text247248### File Information Testing249250```csharp251[Fact]252public void GetFileInfo_File_Exists_Should_Return_Correct_Info()253{254 // Arrange255 var content = "Hello, World!";256 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>257 {258 [@"C:\test.txt"] = new MockFileData(content)259 });260261 var service = new FileManagerService(mockFileSystem);262263 // Act264 var info = service.GetFileInfo(@"C:\test.txt");265266 // Assert267 info.Should().NotBeNull();268 info!.Name.Should().Be("test.txt");269 info.Size.Should().Be(content.Length);270}271```text272273### Backup File Testing274275```csharp276[Fact]277public void BackupFile_File_Exists_Should_Create_Timestamp_Backup()278{279 // Arrange280 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>281 {282 [@"C:\data\important.txt"] = new MockFileData("important data")283 });284285 var service = new FileManagerService(mockFileSystem);286287 // Act288 var backupPath = service.BackupFile(@"C:\data\important.txt");289290 // Assert291 backupPath.Should().StartWith(@"C:\data\important_");292 backupPath.Should().EndWith(".txt");293 mockFileSystem.File.Exists(backupPath).Should().BeTrue();294}295```text296297## Best Practices298299### ✅ Should Do3003011. **Use Path.Combine to handle paths**:302303 ```csharp304 var path = _fileSystem.Path.Combine("configs", "app.json");305```text3063072. **Defensively check file existence**:308309 ```csharp310 if (!_fileSystem.File.Exists(filePath))311 {312 return defaultValue;313 }314```text3153163. **Auto-create necessary directories**:317318 ```csharp319 var dir = _fileSystem.Path.GetDirectoryName(filePath);320 if (!string.IsNullOrEmpty(dir) && !_fileSystem.Directory.Exists(dir))321 {322 _fileSystem.Directory.CreateDirectory(dir);323 }324```text3253264. **Properly handle various IO exceptions**:327328 ```csharp329 try330 {331 return await _fileSystem.File.ReadAllTextAsync(path);332 }333 catch (UnauthorizedAccessException) { /* insufficient permissions */ }334 catch (IOException) { /* file locked */ }335 catch (DirectoryNotFoundException) { /* directory not found */ }336```text3373385. **Use independent MockFileSystem for each test**:339340 ```csharp341 public class ServiceTests342 {343 [Fact]344 public void Test1()345 {346 var mockFs = new MockFileSystem(); // Independent instance347 }348349 [Fact]350 public void Test2()351 {352 var mockFs = new MockFileSystem(); // Independent instance353 }354 }355```text356357### ❌ Should Avoid3583591. **Hardcode path separators**:360361 ```csharp362 // ❌ Don't do this363 var path = "configs\\app.json"; // Windows only364 var path = "configs/app.json"; // Unix only365366 // ✅ Should do this367 var path = _fileSystem.Path.Combine("configs", "app.json");368```text3693702. **Use real file system in unit tests**:371372 ```csharp373 // ❌ This is not a unit test374 var realFs = new FileSystem();375376 // ✅ Unit tests should use MockFileSystem377 var mockFs = new MockFileSystem();378```text3793803. **Ignore exception handling**:381382 ```csharp383 // ❌ Don't assume file always exists384 var content = _fileSystem.File.ReadAllText(path);385386 // ✅ Add existence check and exception handling387 if (_fileSystem.File.Exists(path))388 {389 try { return _fileSystem.File.ReadAllText(path); }390 catch (IOException) { return defaultValue; }391 }392```text393394## Performance Considerations395396### MockFileSystem Advantages397398- **Speed**: 10-100x faster than real file operations399- **Reliability**: Not affected by disk state400- **Isolation**: Complete isolation between tests401- **Error Simulation**: Can precisely simulate various exception scenarios402403### Memory Usage Recommendations404405- Only create files necessary for testing406- Avoid simulating oversized files in tests407- For large file processing logic, use moderately sized test data:408409```csharp410// ✅ Moderately sized test data411var testContent = string.Join("\n",412 Enumerable.Range(1, 1000).Select(i => $"Line {i}"));413mockFileSystem.AddFile("test.txt", new MockFileData(testContent));414```text415416## Practical Integration Examples417418### Configuration File Management Service419420See `templates/configmanager-service.cs` for complete implementation, including:421422- Configuration file load and save423- JSON serialization and deserialization424- Auto-create directories425- Configuration file backup functionality426427### File Management Service428429See `templates/filemanager-service.cs` for implementation, including:430431- File copy and backup432- Directory operations433- File information query434- Error handling patterns435436## Reference Resources437438### Original Articles439440This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:441442- **Day 17 - File and IO Testing: Using System.IO.Abstractions to Simulate File System**443 - Article: https://ithelp.ithome.com.tw/articles/10375981444 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day17445446### Official Documentation447448- [System.IO.Abstractions GitHub](https://github.com/TestableIO/System.IO.Abstractions)449- [System.IO.Abstractions NuGet](https://www.nuget.org/packages/System.IO.Abstractions/)450- [TestingHelpers NuGet](https://www.nuget.org/packages/System.IO.Abstractions.TestingHelpers/)451452### Related Skills453454- `nsubstitute-mocking` - Test doubles and mocking455- `unit-test-fundamentals` - Unit testing basics456````