You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 00:35:18