xiaohongshu
xiaohongshu
dotnet-testing-filesystem-testing-abstractions
使用 System.IO.Abstractions 測試檔案系統操作的專門技能。當需要測試 File、Directory、Path 等操作、模擬檔案系統時使用。涵蓋 IFileSystem、MockFileSystem、檔案讀寫測試、目錄操作測試等。 Keywords: file testing, filesystem, 檔案測試, 檔案系統測試, IFileSystem, MockFileSystem, System.IO.Abstractions, File.ReadAllText, File.WriteAllText, Directory.CreateDirectory, Path...
Full skill instructions
當被要求執行以下任務時,請使用此技能:
System.IO.File、System.IO.Directory 等靜態類別的程式碼傳統直接使用 System.IO 靜態類別的程式碼難以測試,原因包括:
這是一個將 System.IO 靜態類別包裝成介面的套件,支援依賴注入和測試替身。
核心介面架構:
public interface IFileSystem
{
IFile File { get; }
IDirectory Directory { get; }
IFileInfo FileInfo { get; }
IDirectoryInfo DirectoryInfo { get; }
IPath Path { get; }
IDriveInfo DriveInfo { get; }
}
必要 NuGet 套件:
<!-- 正式環境 -->
<PackageReference Include="System.IO.Abstractions" Version="21.*" />
<!-- 測試專案 -->
<PackageReference Include="System.IO.Abstractions.TestingHelpers" Version="21.*" />
步驟一:將直接使用靜態類別的程式碼改為依賴 IFileSystem
// ❌ 重構前(不可測試)
public class ConfigService
{
public string LoadConfig(string path)
{
return File.ReadAllText(path);
}
}
// ✅ 重構後(可測試)
public class ConfigService
{
private readonly IFileSystem _fileSystem;
public ConfigService(IFileSystem fileSystem)
{
_fileSystem = fileSystem;
}
public string LoadConfig(string path)
{
return _fileSystem.File.ReadAllText(path);
}
}
步驟二:在 DI 容器中註冊真實實作
// Program.cs
services.AddSingleton<IFileSystem, FileSystem>();
services.AddScoped<ConfigService>();
步驟三:在測試中使用 MockFileSystem
var mockFs = new MockFileSystem(new Dictionary<string, MockFileData>
{
["config.json"] = new MockFileData("{ \"key\": \"value\" }")
});
var service = new ConfigService(mockFs);
[Fact]
public async Task LoadConfig_檔案存在_應回傳內容()
{
// Arrange - 建立預設的檔案系統狀態
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() // 空目錄
});
var service = new ConfigService(mockFileSystem);
// Act
var result = await service.LoadConfigAsync("config.json");
// Assert
result.Should().Contain("key");
}
[Fact]
public async Task SaveConfig_指定內容_應正確寫入()
{
// Arrange
var mockFileSystem = new MockFileSystem();
var service = new ConfigService(mockFileSystem);
// Act
await service.SaveConfigAsync("output.json", "{ \"saved\": true }");
// Assert - 驗證檔案系統的最終狀態
mockFileSystem.File.Exists("output.json").Should().BeTrue();
var content = await mockFileSystem.File.ReadAllTextAsync("output.json");
content.Should().Contain("saved");
}
[Fact]
public void CopyFile_目標目錄不存在_應自動建立()
{
// 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();
}
當需要模擬特定異常時,MockFileSystem 支援有限,可使用 NSubstitute:
[Fact]
public void TryReadFile_權限不足_應回傳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("存取被拒"));
var service = new FilePermissionService(mockFileSystem);
// Act
var result = service.TryReadFile("protected.txt", out var content);
// Assert
result.Should().BeFalse();
content.Should().BeNull();
}
[Fact]
public async Task CountLines_多行檔案_應回傳正確行數()
{
// 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);
}
[Fact]
public void GetFileInfo_檔案存在_應回傳正確資訊()
{
// 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);
}
[Fact]
public void BackupFile_檔案存在_應建立時間戳記備份()
{
// Arrange
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
[@"C:\data\important.txt"] = new MockFileData("重要資料")
});
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();
}
使用 Path.Combine 處理路徑:
var path = _fileSystem.Path.Combine("configs", "app.json");
防禦性檢查檔案存在性:
if (!_fileSystem.File.Exists(filePath))
{
return defaultValue;
}
自動建立必要目錄:
var dir = _fileSystem.Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir) && !_fileSystem.Directory.Exists(dir))
{
_fileSystem.Directory.CreateDirectory(dir);
}
妥善處理各種 IO 異常:
try
{
return await _fileSystem.File.ReadAllTextAsync(path);
}
catch (UnauthorizedAccessException) { /* 權限不足 */ }
catch (IOException) { /* 檔案被鎖定 */ }
catch (DirectoryNotFoundException) { /* 目錄不存在 */ }
每個測試使用獨立的 MockFileSystem:
public class ServiceTests
{
[Fact]
public void Test1()
{
var mockFs = new MockFileSystem(); // 獨立實例
}
[Fact]
public void Test2()
{
var mockFs = new MockFileSystem(); // 獨立實例
}
}
硬編碼路徑分隔符號:
// ❌ 不要這樣做
var path = "configs\\app.json"; // Windows only
var path = "configs/app.json"; // Unix only
// ✅ 應該這樣做
var path = _fileSystem.Path.Combine("configs", "app.json");
在單元測試中使用真實檔案系統:
// ❌ 這不是單元測試
var realFs = new FileSystem();
// ✅ 單元測試應使用 MockFileSystem
var mockFs = new MockFileSystem();
忽略例外處理:
// ❌ 不要假設檔案一定存在
var content = _fileSystem.File.ReadAllText(path);
// ✅ 加入存在性檢查和例外處理
if (_fileSystem.File.Exists(path))
{
try { return _fileSystem.File.ReadAllText(path); }
catch (IOException) { return defaultValue; }
}
// ✅ 適度大小的測試資料
var testContent = string.Join("\n",
Enumerable.Range(1, 1000).Select(i => $"Line {i}"));
mockFileSystem.AddFile("test.txt", new MockFileData(testContent));
請參考 templates/configmanager-service.cs 中的完整實作,包含:
請參考 templates/filemanager-service.cs 中的實作,包含:
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
nsubstitute-mocking - 測試替身與模擬unit-test-fundamentals - 單元測試基礎xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience