xUnit Mock 从 IoC 容器请求新实例

xUnit Mock request new instance from IoC container

我正在为使用 SSH.Net.

上传文件的方法编写一些单元测试

该项目是一个 WPF 应用程序,使用 Caliburn.Micro 作为 MVVM 框架,还在我正在测试的 class 的构造函数中注入以下对象:

    private IFileTransferManager _fileTransferManager;

    public FileUploadViewModel(IFileTransferManager fileTransferManager) : base(eventAggregator)
    {
        _fileTransferManager = fileTransferManager;
    }

在测试项目中我正在模拟 IFileTransferManager:

    private Mock<IFileTransferManager> _fileTransferManager = new Mock<IFileTransferManager>();

但是现在我说到重点了,在代码中我需要从 IoC 容器请求一个新的 IFileTransferManager 实例,IoC 在 Caliburn.Micro 中是一个静态 class:

    _fileTransferManager = IoC.Get<IFileTransferManager>();

    await _fileTransferManager.UploadFile(connection, file.FullName, destinationPath).ConfigureAwait(false);

我如何重构上面的代码以使其可测试,因为目前它在 Caliburn.Micro.dll 中抛出 System.InvalidOperationException 因为我正在重新实例化 _fileTransferManager

使用 Func<TResult> 委托注入工厂。

private readonly Func<IFileTransferManager> fileTransferManagerFactory;

public FileUploadViewModel(Func<IFileTransferManager> fileTransferManagerFactory) : base(eventAggregator) {
    this.fileTransferManagerFactory = fileTransferManagerFactory;
}

这将允许在上传时根据需要创建尽可能多的实例

//get an instance using factory delegate
var fileTransferManager =  fileTransferManagerFactory();

await fileTransferManager.UploadFile(connection, file.FullName, destinationPath).ConfigureAwait(false); IoC.Get<IFileTransferManager>();

对于单元测试,可以轻松创建一个函数来提供测试用例所需的尽可能多的模拟

我可能会做这样的事情,假设有其他限制因素意味着你想尽可能少地改变 class 的外部细节(注意:我没有测试过这个所以可能有稍微调整一下)

public class ClassIAmTesting
{
    //Have a Func to fetch a file manager...
    private Func<IFileTransferManager> _filemgr = null;
    //Have a property which we'll use in this class to get the File manager
    public Func<IFilterTransferManager> GetFileManager
    {
        get
        {
            //If we try to use this property for the first time and it's not set,
            //then set it to the default implementation.
            if (_fileMgr == null)
            {
                _fileMgr = () => IoC.Get<IFileTransferManager>();
            }
            return _fileMgr;
        }
        set
        {         
            //allow setting of the function which returns an IFileTransferManager
            if (_fileMgr == null)
            {
                _fileMgr = value;
            }
        }
    }

    //this is the method you ultimately want to test...
    public async Task<bool> SomeMethodIAmTesting()
    {
        //don't do this any more:
        //_fileTransferManager = IoC.Get<IFileTransferManager>();

        //instead do this.
        _fileTransferManager = GetFileManager();

        await _fileTransferManager
            .UploadFile(connection, file.FullName, destinationPath)
            .ConfigureAwait(false);

        return true;
    }
}

那么在你的测试中:

Mock<IFileTransferManager> _fileTransferManager = new Mock<IFileTransferManager>();
var cut = new ClassIAmTesting();
//not used Moq for a long time, but think you have to access .Object to get to the 
//actual implementation of IFileTransferManager?
cut.GetFileManager = () => _fileTransferManager.Object; 

//Do actual tests..
var result = cut.SomeMethodIAmTesting();

//Do assertions...

我建议采用这种方法,因为:

  • 它提供了一种方法来覆盖 class 获取 IFileTransferManager 进行测试的方式
  • 它 'falls back' 如果不使用此覆盖,则为默认实现,保留原始行为 - 您根本不需要从非测试代码更改对此 class 的现有调用
  • 它不会更改构造函数或添加新构造函数,我假设是个问题,因为您不只是将 IFileTransferManager 的实例注入其中。

一个改进可能是设置 internal 以防止其他项目设置此方法,然后可以通过 InternalVisibleTo 或类似方式公开它,但我正在努力保持范围相当紧凑...