dotnet-testing-filesystem-testing-abstractions
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-filesystem-testing-abstractions
Agent 安装分布
Skill 文档
æªæ¡ç³»çµ±æ¸¬è©¦ï¼ä½¿ç¨ System.IO.Abstractions æ¨¡æ¬æªæ¡æä½
é©ç¨æ å¢
ç¶è¢«è¦æ±å·è¡ä»¥ä¸ä»»åæï¼è«ä½¿ç¨æ¤æè½ï¼
- éæ§ç´æ¥ä½¿ç¨
System.IO.FileãSystem.IO.Directoryçéæ é¡å¥çç¨å¼ç¢¼ - çºæ¶åæªæ¡è®å¯«ãç®éæä½çç¨å¼ç¢¼æ°å¯«å®å 測試
- ä½¿ç¨ MockFileSystem 模æ¬åç¨®æªæ¡ç³»çµ±çæ
- æ¸¬è©¦æªæ¡æ¬éä¸è¶³ãæªæ¡ä¸åå¨çç°å¸¸æ å¢
- è¨è¨å¯æ¸¬è©¦çæªæ¡èçæåæ¶æ§
æ ¸å¿åå
1. æªæ¡ç³»çµ±ç¸ä¾æ§çæ ¹æ¬åé¡
å³çµ±ç´æ¥ä½¿ç¨ System.IO éæ
é¡å¥çç¨å¼ç¢¼é£ä»¥æ¸¬è©¦ï¼åå å
æ¬ï¼
- é度åé¡ï¼å¯¦éç£ç¢ IO æ¯è¨æ¶é«æä½æ ¢ 10-100 å
- ç°å¢ç¸ä¾ï¼æ¸¬è©¦çµæåæªæ¡ç³»çµ±çæ ãæ¬éãè·¯å¾å½±é¿
- å¯ä½ç¨ï¼æ¸¬è©¦æå¨ç£ç¢ä¸çä¸çè·¡ï¼å½±é¿å ¶ä»æ¸¬è©¦
- 並è¡åé¡ï¼å¤åæ¸¬è©¦åææä½å䏿ªæ¡æç¢çç«¶çæ¢ä»¶
- é¯èª¤æ¨¡æ¬å°é£ï¼é£ä»¥æ¨¡æ¬æ¬éä¸è¶³ãç£ç¢ç©ºéä¸è¶³çç°å¸¸
2. System.IO.Abstractions è§£æ±ºæ¹æ¡
鿝ä¸åå° 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.*" />
3. éæ§æ¥é©
æ¥é©ä¸ï¼å°ç´æ¥ä½¿ç¨éæ
é¡å¥çç¨å¼ç¢¼æ¹çºä¾è³´ 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);
MockFileSystem 測試模å¼
模å¼ä¸ï¼é è¨æªæ¡çæ
[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();
}
模å¼åï¼ä½¿ç¨ NSubstitute 模æ¬é¯èª¤
ç¶éè¦æ¨¡æ¬ç¹å®ç°å¸¸æï¼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; } }
æè½èé
MockFileSystem åªå¢
- éåº¦ï¼æ¯çå¯¦æªæ¡æä½å¿« 10-100 å
- å¯é æ§ï¼ä¸åç£ç¢çæ å½±é¿
- é颿§ï¼æ¸¬è©¦ä¹éå®å ¨éé¢
- é¯èª¤æ¨¡æ¬ï¼å¯ç²¾ç¢ºæ¨¡æ¬å種ç°å¸¸æ å¢
è¨æ¶é«ä½¿ç¨å»ºè°
- åªå»ºç«æ¸¬è©¦å¿ éçæªæ¡
- é¿å 卿¸¬è©¦ä¸æ¨¡æ¬è¶ å¤§æªæ¡
- å°æ¼å¤§æªæ¡èçé輯ï¼ä½¿ç¨é©åº¦å¤§å°çæ¸¬è©¦è³æï¼
// â
é©åº¦å¤§å°çæ¸¬è©¦è³æ
var testContent = string.Join("\n",
Enumerable.Range(1, 1000).Select(i => $"Line {i}"));
mockFileSystem.AddFile("test.txt", new MockFileData(testContent));
坦忴åç¯ä¾
è¨å®æªç®¡çæå
è«åè templates/configmanager-service.cs ä¸ç宿´å¯¦ä½ï¼å
å«ï¼
- è¨å®æªè¼å ¥èå²å
- JSON åºååèååºåå
- èªå建ç«ç®é
- è¨å®æªå份åè½
æªæ¡ç®¡çæå
è«åè templates/filemanager-service.cs ä¸ç實ä½ï¼å
å«ï¼
- æªæ¡è¤è£½èå份
- ç®éæä½
- æªæ¡è³è¨æ¥è©¢
- é¯èª¤èçæ¨¡å¼
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 17 – æªæ¡è IO 測試ï¼ä½¿ç¨ System.IO.Abstractions æ¨¡æ¬æªæ¡ç³»çµ±
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10375981
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day17
宿¹æä»¶
ç¸éæè½
nsubstitute-mocking– 測試æ¿èº«è模æ¬unit-test-fundamentals– å®å 測試åºç¤