如何为含UI控件的C# ExecuteExportCsvCommand方法编写xUnit测试?
解决带UI控件的ViewModel方法单元测试问题
你的核心问题在于ViewModel直接依赖了UI控件(SaveFileDialog、ExportPrintDataDialog),这些控件需要UI环境才能运行,无法在单元测试中模拟。解决思路是先解耦UI依赖,再针对解耦后的逻辑编写测试用例。
第一步:重构ViewModel,抽象UI对话框接口
先把对话框的行为抽象成接口,让ViewModel依赖接口而非具体UI控件:
定义抽象接口
// 保存文件对话框接口 public interface IFileDialogService { // 返回用户选择的文件路径,取消则返回null string? ShowSaveCsvDialog(); // 如果是异步方法,改成Task<string?> ShowSaveCsvDialogAsync(); } // 导出打印数据对话框接口 public interface IPrintDataDialogService { // 返回用户选择的导出选项,取消则返回null ExportPrintDataOptions? ShowExportPrintDataDialog(); // 异步版本:Task<ExportPrintDataOptions?> ShowExportPrintDataDialogAsync(); }
修改ViewModel构造函数与方法逻辑
更新ProjectPrintViewModel,注入上述接口,替换原来直接new对话框的代码:
public class ProjectPrintViewModel { private readonly IFileDialogService _fileDialogService; private readonly IPrintDataDialogService _printDataDialogService; // 构造函数添加新接口参数 public ProjectPrintViewModel( OptionsContainer options, ProjectViewModel project, ILogService logService, IPermissionChecker permissionChecker, IAuditService auditService, IProjectService projectService, IValuePointService valueService, bool isPreview, IFileDialogService fileDialogService, IPrintDataDialogService printDataDialogService) { _fileDialogService = fileDialogService; _printDataDialogService = printDataDialogService; // 其他初始化逻辑... } public void ExecuteExportCsvCommand() { // 先检查权限 if (!_permissionChecker.HasPermission(PermissionType.ExportCsv)) { _logService.Warn("无CSV导出权限"); return; } // 调用接口方法,而非直接new SaveFileDialog var filePath = _fileDialogService.ShowSaveCsvDialog(); if (filePath == null) { _logService.Info("用户取消保存文件选择"); return; } // 调用导出数据对话框接口 var exportOptions = _printDataDialogService.ShowExportPrintDataDialog(); if (exportOptions == null) { _logService.Info("用户取消导出选项设置"); return; } // 执行导出逻辑 try { _projectService.ExportToCsv(filePath, exportOptions); _logService.Info($"CSV导出成功,路径:{filePath}"); _auditService.LogAudit("导出CSV", $"导出文件:{filePath}"); } catch (Exception ex) { _logService.Error("CSV导出失败", ex); } } }
第二步:更新测试类,Mock抽象接口
在你的测试类中添加Mock接口实例,并注入到ViewModel:
public class ProjectPrintViewModelTests { private readonly ProjectPrintViewModel _sut; private readonly IHost _host; private readonly Mock<IPermissionChecker> _permissionChecker = new Mock<IPermissionChecker>(); private readonly Mock<ILogService> _logService = new Mock<ILogService>(); private readonly Mock<IAuditService> _auditService = new Mock<IAuditService>(); private readonly Mock<IProjectService> _projectService = new Mock<IProjectService>(); // 添加新的Mock接口 private readonly Mock<IFileDialogService> _fileDialogService = new Mock<IFileDialogService>(); private readonly Mock<IPrintDataDialogService> _printDataDialogService = new Mock<IPrintDataDialogService>(); public ProjectPrintViewModelTests() { var colorList = new List<string> { "DefaultMinColor", "DefaultLowColor", "DefaultNormColor", "DefaultHighColor", "DefaultMaxColor" }; var app = new Application(); colorList.ForEach(x => app.Resources.Add(x, Color.FromRgb(255, 255, 255))); _host = MockEnvironment.BuildHost(); IoCContainer.SetServiceProvider(_host.Services); var options = _host.Services.GetRequiredService<OptionsContainer>(); var valueService = _host.Services.GetRequiredService<IValuePointService>(); var project = new ProjectViewModel(options); _sut = new ProjectPrintViewModel( options, project, _logService.Object, _permissionChecker.Object, _auditService.Object, _projectService.Object, valueService, false, _fileDialogService.Object, // 注入Mock的文件对话框服务 _printDataDialogService.Object); // 注入Mock的导出对话框服务 } // 编写测试用例
第三步:编写不同场景的测试用例
场景1:权限通过、用户选择路径和导出选项,导出成功
[Fact] public void ExecuteExportCsvCommand_WhenAllStepsValid_ExportsSuccessfully() { // Assemble var testFilePath = "C:\\test.csv"; var testExportOptions = new ExportPrintDataOptions { // 填充测试需要的属性,比如IncludeHeaders = true等 }; // 模拟权限通过 _permissionChecker.Setup(p => p.HasPermission(PermissionType.ExportCsv)).Returns(true); // 模拟用户选择了文件路径 _fileDialogService.Setup(s => s.ShowSaveCsvDialog()).Returns(testFilePath); // 模拟用户确认了导出选项 _printDataDialogService.Setup(s => s.ShowExportPrintDataDialog()).Returns(testExportOptions); // Act _sut.ExecuteExportCsvCommand(); // Assert // 验证权限检查被调用 _permissionChecker.Verify(p => p.HasPermission(PermissionType.ExportCsv), Times.Once); // 验证文件对话框被调用 _fileDialogService.Verify(s => s.ShowSaveCsvDialog(), Times.Once); // 验证导出选项对话框被调用 _printDataDialogService.Verify(s => s.ShowExportPrintDataDialog(), Times.Once); // 验证导出服务被调用,参数匹配 _projectService.Verify(s => s.ExportToCsv(testFilePath, testExportOptions), Times.Once); // 验证日志记录了成功信息 _logService.Verify(l => l.Info(It.Is<string>(msg => msg.Contains("CSV导出成功"))), Times.Once); // 验证审计日志被记录 _auditService.Verify(a => a.LogAudit("导出CSV", It.Is<string>(msg => msg.Contains(testFilePath))), Times.Once); }
场景2:用户取消保存文件对话框,导出终止
[Fact] public void ExecuteExportCsvCommand_WhenUserCancelsSaveDialog_DoesNotProceedToExport() { // Assemble _permissionChecker.Setup(p => p.HasPermission(PermissionType.ExportCsv)).Returns(true); // 模拟用户取消保存对话框 _fileDialogService.Setup(s => s.ShowSaveCsvDialog()).Returns(null); // Act _sut.ExecuteExportCsvCommand(); // Assert _permissionChecker.Verify(p => p.HasPermission(PermissionType.ExportCsv), Times.Once); _fileDialogService.Verify(s => s.ShowSaveCsvDialog(), Times.Once); // 验证导出选项对话框和导出服务都未被调用 _printDataDialogService.Verify(s => s.ShowExportPrintDataDialog(), Times.Never); _projectService.Verify(s => s.ExportToCsv(It.IsAny<string>(), It.IsAny<ExportPrintDataOptions>()), Times.Never); // 验证日志记录了取消信息 _logService.Verify(l => l.Info("用户取消保存文件选择"), Times.Once); }
场景3:无导出权限,直接终止流程
[Fact] public void ExecuteExportCsvCommand_WhenNoPermission_DoesNotShowDialogs() { // Assemble _permissionChecker.Setup(p => p.HasPermission(PermissionType.ExportCsv)).Returns(false); // Act _sut.ExecuteExportCsvCommand(); // Assert _permissionChecker.Verify(p => p.HasPermission(PermissionType.ExportCsv), Times.Once); // 验证所有对话框和导出服务都未被调用 _fileDialogService.Verify(s => s.ShowSaveCsvDialog(), Times.Never); _printDataDialogService.Verify(s => s.ShowExportPrintDataDialog(), Times.Never); _projectService.Verify(s => s.ExportToCsv(It.IsAny<string>(), It.IsAny<ExportPrintDataOptions>()), Times.Never); // 验证日志记录了权限不足信息 _logService.Verify(l => l.Warn("无CSV导出权限"), Times.Once); }
场景4:用户取消导出选项对话框,导出终止
[Fact] public void ExecuteExportCsvCommand_WhenUserCancelsExportDialog_DoesNotCallExport() { // Assemble _permissionChecker.Setup(p => p.HasPermission(PermissionType.ExportCsv)).Returns(true); _fileDialogService.Setup(s => s.ShowSaveCsvDialog()).Returns("C:\\test.csv"); // 模拟用户取消导出选项对话框 _printDataDialogService.Setup(s => s.ShowExportPrintDataDialog()).Returns(null); // Act _sut.ExecuteExportCsvCommand(); // Assert _permissionChecker.Verify(p => p.HasPermission(PermissionType.ExportCsv), Times.Once); _fileDialogService.Verify(s => s.ShowSaveCsvDialog(), Times.Once); _printDataDialogService.Verify(s => s.ShowExportPrintDataDialog(), Times.Once); _projectService.Verify(s => s.ExportToCsv(It.IsAny<string>(), It.IsAny<ExportPrintDataOptions>()), Times.Never); _logService.Verify(l => l.Info("用户取消导出选项设置"), Times.Once); }
注意事项
- 如果你的
ExecuteExportCsvCommand是异步方法(比如返回Task),测试方法要改成async Task,并在Act步骤使用await,同时Mock接口的异步方法。 - 实际项目中需要编写接口的具体实现类(比如
FileDialogService),在运行时通过DI注入,测试时用Mock替代。
内容的提问来源于stack exchange,提问作者mkhasan
相关产品推荐
相关产品推荐

