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-abstractions3description: 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 abstraction4license: MIT5---67Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.89# File System Testing: Using System.IO.Abstractions to Simulate File Operations1011## Applicable Scenarios1213Use this skill when asked to perform the following tasks:1415- Refactor code directly using `System.IO.File`, `System.IO.Directory` and other static classes16- Write unit tests for code involving file read/write, directory operations17- Use MockFileSystem to simulate various file system states18- Test exception scenarios like insufficient file permissions, file not found19- Design testable file processing service architecture2021## Core Principles2223### 1. Fundamental Problem of File System Dependencies2425Traditional code directly using `System.IO` static classes is difficult to test, reasons include:2627- **Speed Issues**: Actual disk IO is 10-100x slower than memory operations28- **Environment Dependency**: Test results affected by file system state, permissions, paths29- **Side Effects**: Tests leave traces on disk, affecting other tests30- **Concurrency Issues**: Multiple tests operating on same file create race conditions31- **Error Simulation Difficulty**: Difficult to simulate insufficient permissions, insufficient disk space, etc.3233### 2. System.IO.Abstractions Solution3435This is a package that wraps System.IO static classes into interfaces, supporting dependency injection and test doubles.3637**Core Interface Architecture**:3839````csharp40public interface IFileSystem41{42 IFile File { get; }43 IDirectory Directory { get; }44 IFileInfo FileInfo { get; }45 IDirectoryInfo DirectoryInfo { get; }46 IPath Path { get; }47 IDriveInfo DriveInfo { get; }48}49```text5051**Required NuGet Packages**:5253```xml54<!-- Production environment -->55<PackageReference Include="System.IO.Abstractions" Version="21.*" />5657<!-- Test project -->58<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.*" />59```text6061### 3. Refactoring Steps6263**Step 1**: Change code directly using static classes to depend on `IFileSystem`6465```csharp66// ❌ Before refactoring (not testable)67public class ConfigService68{69 public string LoadConfig(string path)70 {71 return File.ReadAllText(path);72 }73}7475// ✅ After refactoring (testable)76public class ConfigService77{78 private readonly IFileSystem _fileSystem;7980 public ConfigService(IFileSystem fileSystem)81 {82 _fileSystem = fileSystem;83 }8485 public string LoadConfig(string path)86 {87 return _fileSystem.File.ReadAllText(path);88 }89}90```text9192**Step 2**: Register real implementation in DI container9394```csharp95// Program.cs96services.AddSingleton<IFileSystem, FileSystem>();97services.AddScoped<ConfigService>();98```text99100**Step 3**: Use MockFileSystem in tests101102```csharp103var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>104{105 ["config.json"] = new MockFileData("{ \"key\": \"value\" }")106});107var service = new ConfigService(mockFs);108```text109110## MockFileSystem Testing Patterns111112### Pattern 1: Default File State113114```csharp115[Fact]116public async Task LoadConfig_File_Exists_Should_Return_Content()117{118 // Arrange - Create default file system state119 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>120 {121 ["config.json"] = new MockFileData("{ \"key\": \"value\" }"),122 [@"C:\data\users.csv"] = new MockFileData("Name,Age\nJohn,25"),123 [@"C:\logs\"] = new MockDirectoryData() // Empty directory124 });125126 var service = new ConfigService(mockFileSystem);127128 // Act129 var result = await service.LoadConfigAsync("config.json");130131 // Assert132 result.Should().Contain("key");133}134```text135136### Pattern 2: Verify Write Results137138```csharp139[Fact]140public async Task SaveConfig_Specified_Content_Should_Write_Correctly()141{142 // Arrange143 var mockFileSystem = new MockFileSystem();144 var service = new ConfigService(mockFileSystem);145146 // Act147 await service.SaveConfigAsync("output.json", "{ \"saved\": true }");148149 // Assert - Verify final state of file system150 mockFileSystem.File.Exists("output.json").Should().BeTrue();151 var content = await mockFileSystem.File.ReadAllTextAsync("output.json");152 content.Should().Contain("saved");153}154```text155156### Pattern 3: Test Directory Operations157158```csharp159[Fact]160public void CopyFile_Target_Directory_Not_Exists_Should_Auto_Create()161{162 // Arrange163 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>164 {165 [@"C:\source\file.txt"] = new MockFileData("content")166 });167 var service = new FileManagerService(mockFileSystem);168169 // Act170 service.CopyFileToDirectory(@"C:\source\file.txt", @"C:\target\subfolder");171172 // Assert173 mockFileSystem.Directory.Exists(@"C:\target\subfolder").Should().BeTrue();174 mockFileSystem.File.Exists(@"C:\target\subfolder\file.txt").Should().BeTrue();175}176```text177178### Pattern 4: Use NSubstitute to Simulate Errors179180When needing to simulate specific exceptions, MockFileSystem has limited support, can use NSubstitute:181182```csharp183[Fact]184public void TryReadFile_Insufficient_Permissions_Should_Return_False()185{186 // Arrange187 var mockFileSystem = Substitute.For<IFileSystem>();188 var mockFile = Substitute.For<IFile>();189190 mockFileSystem.File.Returns(mockFile);191 mockFile.Exists("protected.txt").Returns(true);192 mockFile.ReadAllText("protected.txt")193 .Throws(new UnauthorizedAccessException("Access denied"));194195 var service = new FilePermissionService(mockFileSystem);196197 // Act198 var result = service.TryReadFile("protected.txt", out var content);199200 // Assert201 result.Should().BeFalse();202 content.Should().BeNull();203}204```text205206## Advanced Testing Techniques207208### Stream Operation Testing209210```csharp211[Fact]212public async Task CountLines_Multi_Line_File_Should_Return_Correct_Count()213{214 // Arrange215 var content = "Line 1\nLine 2\nLine 3\nLine 4";216 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>217 {218 ["data.txt"] = new MockFileData(content)219 });220221 var processor = new StreamProcessorService(mockFileSystem);222223 // Act224 var result = await processor.CountLinesAsync("data.txt");225226 // Assert227 result.Should().Be(4);228}229```text230231### File Information Testing232233```csharp234[Fact]235public void GetFileInfo_File_Exists_Should_Return_Correct_Info()236{237 // Arrange238 var content = "Hello, World!";239 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>240 {241 [@"C:\test.txt"] = new MockFileData(content)242 });243244 var service = new FileManagerService(mockFileSystem);245246 // Act247 var info = service.GetFileInfo(@"C:\test.txt");248249 // Assert250 info.Should().NotBeNull();251 info!.Name.Should().Be("test.txt");252 info.Size.Should().Be(content.Length);253}254```text255256### Backup File Testing257258```csharp259[Fact]260public void BackupFile_File_Exists_Should_Create_Timestamp_Backup()261{262 // Arrange263 var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>264 {265 [@"C:\data\important.txt"] = new MockFileData("important data")266 });267268 var service = new FileManagerService(mockFileSystem);269270 // Act271 var backupPath = service.BackupFile(@"C:\data\important.txt");272273 // Assert274 backupPath.Should().StartWith(@"C:\data\important_");275 backupPath.Should().EndWith(".txt");276 mockFileSystem.File.Exists(backupPath).Should().BeTrue();277}278```text279280## Best Practices281282### ✅ Should Do2832841. **Use Path.Combine to handle paths**:285286 ```csharp287 var path = _fileSystem.Path.Combine("configs", "app.json");288```text2892902. **Defensively check file existence**:291292 ```csharp293 if (!_fileSystem.File.Exists(filePath))294 {295 return defaultValue;296 }297```text2982993. **Auto-create necessary directories**:300301 ```csharp302 var dir = _fileSystem.Path.GetDirectoryName(filePath);303 if (!string.IsNullOrEmpty(dir) && !_fileSystem.Directory.Exists(dir))304 {305 _fileSystem.Directory.CreateDirectory(dir);306 }307```text3083094. **Properly handle various IO exceptions**:310311 ```csharp312 try313 {314 return await _fileSystem.File.ReadAllTextAsync(path);315 }316 catch (UnauthorizedAccessException) { /* insufficient permissions */ }317 catch (IOException) { /* file locked */ }318 catch (DirectoryNotFoundException) { /* directory not found */ }319```text3203215. **Use independent MockFileSystem for each test**:322323 ```csharp324 public class ServiceTests325 {326 [Fact]327 public void Test1()328 {329 var mockFs = new MockFileSystem(); // Independent instance330 }331332 [Fact]333 public void Test2()334 {335 var mockFs = new MockFileSystem(); // Independent instance336 }337 }338```text339340### ❌ Should Avoid3413421. **Hardcode path separators**:343344 ```csharp345 // ❌ Don't do this346 var path = "configs\\app.json"; // Windows only347 var path = "configs/app.json"; // Unix only348349 // ✅ Should do this350 var path = _fileSystem.Path.Combine("configs", "app.json");351```text3523532. **Use real file system in unit tests**:354355 ```csharp356 // ❌ This is not a unit test357 var realFs = new FileSystem();358359 // ✅ Unit tests should use MockFileSystem360 var mockFs = new MockFileSystem();361```text3623633. **Ignore exception handling**:364365 ```csharp366 // ❌ Don't assume file always exists367 var content = _fileSystem.File.ReadAllText(path);368369 // ✅ Add existence check and exception handling370 if (_fileSystem.File.Exists(path))371 {372 try { return _fileSystem.File.ReadAllText(path); }373 catch (IOException) { return defaultValue; }374 }375```text376377## Performance Considerations378379### MockFileSystem Advantages380381- **Speed**: 10-100x faster than real file operations382- **Reliability**: Not affected by disk state383- **Isolation**: Complete isolation between tests384- **Error Simulation**: Can precisely simulate various exception scenarios385386### Memory Usage Recommendations387388- Only create files necessary for testing389- Avoid simulating oversized files in tests390- For large file processing logic, use moderately sized test data:391392```csharp393// ✅ Moderately sized test data394var testContent = string.Join("\n",395 Enumerable.Range(1, 1000).Select(i => $"Line {i}"));396mockFileSystem.AddFile("test.txt", new MockFileData(testContent));397```text398399## Practical Integration Examples400401### Configuration File Management Service402403See `templates/configmanager-service.cs` for complete implementation, including:404405- Configuration file load and save406- JSON serialization and deserialization407- Auto-create directories408- Configuration file backup functionality409410### File Management Service411412See `templates/filemanager-service.cs` for implementation, including:413414- File copy and backup415- Directory operations416- File information query417- Error handling patterns418419## Reference Resources420421### Original Articles422423This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:424425- **Day 17 - File and IO Testing: Using System.IO.Abstractions to Simulate File System**426 - Article: https://ithelp.ithome.com.tw/articles/10375981427 - Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day17428429### Official Documentation430431- [System.IO.Abstractions GitHub](https://github.com/TestableIO/System.IO.Abstractions)432- [System.IO.Abstractions NuGet](https://www.nuget.org/packages/System.IO.Abstractions/)433- [TestingHelpers NuGet](https://www.nuget.org/packages/System.IO.Abstractions.TestingHelpers/)434435### Related Skills436437- `nsubstitute-mocking` - Test doubles and mocking438- `unit-test-fundamentals` - Unit testing basics439````