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-abstractions-33description: "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. 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 abstraction"4---5Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.67# File System Testing: Using System.IO.Abstractions to Simulate File Operations89## Applicable Scenarios1011Use this skill when asked to perform the following tasks:1213- Refactor code directly using `System.IO.File`, `System.IO.Directory` and other static classes14- Write unit tests for code involving file read/write, directory operations15- Use MockFileSystem to simulate various file system states16- Test exception scenarios like insufficient file permissions, file not found17- Design testable file processing service architecture1819## Core Principles2021### 1. Fundamental Problem of File System Dependencies2223Traditional code directly using `System.IO` static classes is difficult to test, reasons include:2425- **Speed Issues**: Actual disk IO is 10-100x slower than memory operations26- **Environment Dependency**: Test results affected by file system state, permissions, paths27- **Side Effects**: Tests leave traces on disk, affecting other tests28- **Concurrency Issues**: Multiple tests operating on same file create race conditions29- **Error Simulation Difficulty**: Difficult to simulate insufficient permissions, insufficient disk space, etc.3031### 2. System.IO.Abstractions Solution3233This is a package that wraps System.IO static classes into interfaces, supporting dependency injection and test doubles.3435**Core Interface Architecture**:3637````csharp38public interface IFileSystem39{40 IFile File { get; }41 IDirectory Directory { get; }42 IFileInfo FileInfo { get; }43 IDirectoryInfo DirectoryInfo { get; }44 IPath Path { get; }45 IDriveInfo DriveInfo { get; }46}47```text4849**Required NuGet Packages**:5051```xml52<!-- Production environment -->53<PackageReference Include="System.IO.Abstractions" Version="21.*" />5455<!-- Test project -->56<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.*" />57```text5859### 3. Refactoring Steps6061**Step 1**: Change code directly using static classes to depend on `IFileSystem`6263```csharp64// ❌ Before refactoring (not testable)65public class ConfigService66{67 public string LoadConfig(string path)68 {69 return File.ReadAllText(path);70 }71}7273// ✅ After refactoring (testable)74public class ConfigService75{76 private readonly IFileSystem _fileSystem;7778 public ConfigService(IFileSystem fileSystem)79 {80 _fileSystem = fileSystem;81 }8283 public string LoadConfig(string path)84 {85 return _fileSystem.File.ReadAllText(path);86 }87}88```text8990**Step 2**: Register real implementation in DI container9192```csharp93// Program.cs94services.AddSingleton<IFileSystem, FileSystem>();95services.AddScoped<ConfigService>();96```text9798**Step 3**: Use MockFileSystem in tests99100```csharp101var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>102{103 ["config.json"] = new MockFileData("{ \"key\": \"value\" }")104});105var service = new ConfigService(mockFs);106```text107108## MockFileSystem Testing Patterns109110### Pattern 1: Default File State111112```csharp113[Fact]114public async Task LoadConfig_File_Exists_Should_Return_Content()115{116 // Arrange - Create default file system state117 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>118 {119 ["config.json"] = new MockFileData("{ \"key\": \"value\" }"),120 [@"C:\data\users.csv"] = new MockFileData("Name,Age\nJohn,25"),121 [@"C:\logs\"] = new MockDirectoryData() // Empty directory122 });123124 var service = new ConfigService(mockFileSystem);125126 // Act127 var result = await service.LoadConfigAsync("config.json");128129 // Assert130 result.Should().Contain("key");131}132```text133134### Pattern 2: Verify Write Results135136```csharp137[Fact]138public async Task SaveConfig_Specified_Content_Should_Write_Correctly()139{140 // Arrange141 var mockFileSystem = new MockFileSystem();142 var service = new ConfigService(mockFileSystem);143144 // Act145 await service.SaveConfigAsync("output.json", "{ \"saved\": true }");146147 // Assert - Verify final state of file system148 mockFileSystem.File.Exists("output.json").Should().BeTrue();149 var content = await mockFileSystem.File.ReadAllTextAsync("output.json");150 content.Should().Contain("saved");151}152```text153154### Pattern 3: Test Directory Operations155156```csharp157[Fact]158public void CopyFile_Target_Directory_Not_Exists_Should_Auto_Create()159{160 // Arrange161 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>162 {163 [@"C:\source\file.txt"] = new MockFileData("content")164 });165 var service = new FileManagerService(mockFileSystem);166167 // Act168 service.CopyFileToDirectory(@"C:\source\file.txt", @"C:\target\subfolder");169170 // Assert171 mockFileSystem.Directory.Exists(@"C:\target\subfolder").Should().BeTrue();172 mockFileSystem.File.Exists(@"C:\target\subfolder\file.txt").Should().BeTrue();173}174```text175176### Pattern 4: Use NSubstitute to Simulate Errors177178When needing to simulate specific exceptions, MockFileSystem has limited support, can use NSubstitute:179180```csharp181[Fact]182public void TryReadFile_Insufficient_Permissions_Should_Return_False()183{184 // Arrange185 var mockFileSystem = Substitute.For<IFileSystem>();186 var mockFile = Substitute.For<IFile>();187188 mockFileSystem.File.Returns(mockFile);189 mockFile.Exists("protected.txt").Returns(true);190 mockFile.ReadAllText("protected.txt")191 .Throws(new UnauthorizedAccessException("Access denied"));192193 var service = new FilePermissionService(mockFileSystem);194195 // Act196 var result = service.TryReadFile("protected.txt", out var content);197198 // Assert199 result.Should().BeFalse();200 content.Should().BeNull();201}202```text203204## Advanced Testing Techniques205206### Stream Operation Testing207208```csharp209[Fact]210public async Task CountLines_Multi_Line_File_Should_Return_Correct_Count()211{212 // Arrange213 var content = "Line 1\nLine 2\nLine 3\nLine 4";214 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>215 {216 ["data.txt"] = new MockFileData(content)217 });218219 var processor = new StreamProcessorService(mockFileSystem);220221 // Act222 var result = await processor.CountLinesAsync("data.txt");223224 // Assert225 result.Should().Be(4);226}227```text228229### File Information Testing230231```csharp232[Fact]233public void GetFileInfo_File_Exists_Should_Return_Correct_Info()234{235 // Arrange236 var content = "Hello, World!";237 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>238 {239 [@"C:\test.txt"] = new MockFileData(content)240 });241242 var service = new FileManagerService(mockFileSystem);243244 // Act245 var info = service.GetFileInfo(@"C:\test.txt");246247 // Assert248 info.Should().NotBeNull();249 info!.Name.Should().Be("test.txt");250 info.Size.Should().Be(content.Length);251}252```text253254### Backup File Testing255256```csharp257[Fact]258public void BackupFile_File_Exists_Should_Create_Timestamp_Backup()259{260 // Arrange261 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>262 {263 [@"C:\data\important.txt"] = new MockFileData("important data")264 });265266 var service = new FileManagerService(mockFileSystem);267268 // Act269 var backupPath = service.BackupFile(@"C:\data\important.txt");270271 // Assert272 backupPath.Should().StartWith(@"C:\data\important_");273 backupPath.Should().EndWith(".txt");274 mockFileSystem.File.Exists(backupPath).Should().BeTrue();275}276```text277278## Best Practices279280### ✅ Should Do2812821. **Use Path.Combine to handle paths**:283284 ```csharp285 var path = _fileSystem.Path.Combine("configs", "app.json");286```text2872882. **Defensively check file existence**:289290 ```csharp291 if (!_fileSystem.File.Exists(filePath))292 {293 return defaultValue;294 }295```text2962973. **Auto-create necessary directories**:298299 ```csharp300 var dir = _fileSystem.Path.GetDirectoryName(filePath);301 if (!string.IsNullOrEmpty(dir) && !_fileSystem.Directory.Exists(dir))302 {303 _fileSystem.Directory.CreateDirectory(dir);304 }305```text3063074. **Properly handle various IO exceptions**:308309 ```csharp310 try311 {312 return await _fileSystem.File.ReadAllTextAsync(path);313 }314 catch (UnauthorizedAccessException) { /* insufficient permissions */ }315 catch (IOException) { /* file locked */ }316 catch (DirectoryNotFoundException) { /* directory not found */ }317```text3183195. **Use independent MockFileSystem for each test**:320321 ```csharp322 public class ServiceTests323 {324 [Fact]325 public void Test1()326 {327 var mockFs = new MockFileSystem(); // Independent instance328 }329330 [Fact]331 public void Test2()332 {333 var mockFs = new MockFileSystem(); // Independent instance334 }335 }336```text337338### ❌ Should Avoid3393401. **Hardcode path separators**:341342 ```csharp343 // ❌ Don't do this344 var path = "configs\\app.json"; // Windows only345 var path = "configs/app.json"; // Unix only346347 // ✅ Should do this348 var path = _fileSystem.Path.Combine("configs", "app.json");349```text3503512. **Use real file system in unit tests**:352353 ```csharp354 // ❌ This is not a unit test355 var realFs = new FileSystem();356357 // ✅ Unit tests should use MockFileSystem358 var mockFs = new MockFileSystem();359```text3603613. **Ignore exception handling**:362363 ```csharp364 // ❌ Don't assume file always exists365 var content = _fileSystem.File.ReadAllText(path);366367 // ✅ Add existence check and exception handling368 if (_fileSystem.File.Exists(path))369 {370 try { return _fileSystem.File.ReadAllText(path); }371 catch (IOException) { return defaultValue; }372 }373```text374375## Performance Considerations376377### MockFileSystem Advantages378379- **Speed**: 10-100x faster than real file operations380- **Reliability**: Not affected by disk state381- **Isolation**: Complete isolation between tests382- **Error Simulation**: Can precisely simulate various exception scenarios383384### Memory Usage Recommendations385386- Only create files necessary for testing387- Avoid simulating oversized files in tests388- For large file processing logic, use moderately sized test data:389390```csharp391// ✅ Moderately sized test data392var testContent = string.Join("\n",393 Enumerable.Range(1, 1000).Select(i => $"Line {i}"));394mockFileSystem.AddFile("test.txt", new MockFileData(testContent));395```text396397## Practical Integration Examples398399### Configuration File Management Service400401See `templates/configmanager-service.cs` for complete implementation, including:402403- Configuration file load and save404- JSON serialization and deserialization405- Auto-create directories406- Configuration file backup functionality407408### File Management Service409410See `templates/filemanager-service.cs` for implementation, including:411412- File copy and backup413- Directory operations414- File information query415- Error handling patterns416417## Reference Resources418419### Original Articles420421This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:422423- **Day 17 - File and IO Testing: Using System.IO.Abstractions to Simulate File System**424 - Article: https://ithelp.ithome.com.tw/articles/10375981425 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day17426427### Official Documentation428429- [System.IO.Abstractions GitHub](https://github.com/TestableIO/System.IO.Abstractions)430- [System.IO.Abstractions NuGet](https://www.nuget.org/packages/System.IO.Abstractions/)431- [TestingHelpers NuGet](https://www.nuget.org/packages/System.IO.Abstractions.TestingHelpers/)432433### Related Skills434435- `nsubstitute-mocking` - Test doubles and mocking436- `unit-test-fundamentals` - Unit testing basics437````