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

使用Rhino Mock单元测试DelegateCommand时出现NullReferenceException如何解决

问题原因及修复方案

直接报错原因

你当前抛出NullReferenceException是两个低级错误直接导致的:

  • 你Mock的是IConvertFileDialogViewModel接口,Rhino Mock生成的模拟对象所有属性默认值都是null,你没有给SelectInputFolderCommand设置模拟值就直接调用Execute(),自然触发空引用
  • 接口定义和实现不匹配:你贴出的接口名是IConvertInterviewDialogViewModel,但ViewModel实现的是IConvertFileDialogViewModel,命名不一致会导致后续依赖注入、Mock匹配全部失效

核心逻辑错误

你的单元测试逻辑完全走偏了:

  • 你要测试的是ConvertFileDialogViewModel的实际业务逻辑,完全不需要Mock ViewModel本身,Mock的是ViewModel的外部依赖
  • 你的ViewModel直接在方法里new了FolderBrowserDialog UI组件,单元测试运行时没有UI上下文,就算不Mock直接实例化ViewModel跑测试,也会弹出系统文件夹选择框,根本做不到自动化运行

完整修复步骤

1. 统一接口命名,抽象UI依赖

先把文件夹选择逻辑抽象成服务接口,解耦ViewModel和UI组件:

// 统一后的ViewModel接口
public interface IConvertFileDialogViewModel
{
    string InputFolderPath { get; set; }
    DelegateCommand SelectInputFolderCommand { get; set; }
}

// 文件夹选择服务接口
public interface IFolderBrowserService
{
    // 返回用户选择的路径,取消则返回null
    string ShowDialog();
}

// 实际运行时的服务实现
public class FolderBrowserService : IFolderBrowserService
{
    public string ShowDialog()
    {
        using var fbd = new FolderBrowserDialog();
        return fbd.ShowDialog() == DialogResult.OK ? fbd.SelectedPath : null;
    }
}

2. 改造ViewModel,使用依赖注入

把服务通过构造函数注入到ViewModel,不要直接new UI组件:

public class ConvertFileDialogViewModel : IConvertFileDialogViewModel, INotifyPropertyChanged
{
    private readonly IFolderBrowserService _folderBrowserService;
    private string _inputFolderPath;

    public DelegateCommand SelectInputFolderCommand { get; set; }
        
    public ConvertFileDialogViewModel(IFolderBrowserService folderBrowserService)
    {
        _folderBrowserService = folderBrowserService ?? throw new ArgumentNullException(nameof(folderBrowserService));
        SelectInputFolderCommand = new DelegateCommand(SelectInputFolderDialog);
    }

    public string InputFolderPath
    {
        get => _inputFolderPath;
        set
        {
            _inputFolderPath = value;
            OnPropertyChanged(nameof(InputFolderPath));
        }
    }
        
    public void SelectInputFolderDialog()
    {
        var selectedPath = _folderBrowserService.ShowDialog();
        if (!string.IsNullOrWhiteSpace(selectedPath))
        {
            InputFolderPath = selectedPath;
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

3. 编写正确的单元测试

这次只Mock外部依赖IFolderBrowserService,ViewModel直接实例化测试:

[TestMethod]
public void SelectInputFolderCommand_UserSelectsValidPath_InputFolderPathUpdated()
{
    // 模拟用户选择了有效路径
    var mockFolderService = MockRepository.GenerateMock<IFolderBrowserService>();
    const string testPath = @"C:\TestFolder";
    mockFolderService.Stub(s => s.ShowDialog()).Return(testPath);

    var viewModel = new ConvertFileDialogViewModel(mockFolderService);
    viewModel.SelectInputFolderCommand.Execute(null);

    // 断言路径正确更新,服务被调用一次
    Assert.AreEqual(testPath, viewModel.InputFolderPath);
    mockFolderService.AssertWasCalled(s => s.ShowDialog(), opt => opt.Repeat.Once());
}

[TestMethod]
public void SelectInputFolderCommand_UserCancelsSelect_InputFolderPathUnchanged()
{
    // 模拟用户点了取消,返回null
    var mockFolderService = MockRepository.GenerateMock<IFolderBrowserService>();
    mockFolderService.Stub(s => s.ShowDialog()).Return(null);

    var viewModel = new ConvertFileDialogViewModel(mockFolderService);
    const string originalPath = @"C:\OldPath";
    viewModel.InputFolderPath = originalPath;

    viewModel.SelectInputFolderCommand.Execute(null);

    // 断言路径没有变化
    Assert.AreEqual(originalPath, viewModel.InputFolderPath);
}

内容的提问来源于stack exchange,提问作者MakkaCha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.02 07:39:04