79

我有一个Transfer类,简化它看起来像这样:

public class Transfer
{
    public virtual IFileConnection source { get; set; }
    public virtual IFileConnection destination { get; set; }

    public virtual void GetFile(IFileConnection connection, 
        string remoteFilename, string localFilename)
    {
        connection.Get(remoteFilename, localFilename);
    }

    public virtual void PutFile(IFileConnection connection, 
        string localFilename, string remoteFilename)
    {
        connection.Get(remoteFilename, localFilename);
    }

    public virtual void TransferFiles(string sourceName, string destName)
    {
        source = internalConfig.GetFileConnection("source");
        destination = internalConfig.GetFileConnection("destination");
        var tempName = Path.GetTempFileName();
        GetFile(source, sourceName, tempName);
        PutFile(destination, tempName, destName);
    }
}

界面的简化版本IFileConnection如下所示:

public interface IFileConnection
{
    void Get(string remoteFileName, string localFileName);
    void Put(string localFileName, string remoteFileName);
}

真正的类应该处理System.IO.IOExceptionIFileConnection具体类失去与远程的连接时抛出的一个,发送电子邮件等等。

我想使用 Moq 创建一个Transfer类,并将其用作我Transfer在所有属性和方法中的具体类,除非GetFile调用该方法 - 然后我希望它抛出 aSystem.IO.IOException并确保Transfer该类正确处理它。

我是否使用了正确的工具来完成这项工作?我会以正确的方式解决这个问题吗?我将如何为该单元测试编写设置NUnit

4

3 回答 3

103

这是您可以模拟的方法FileConnection

Mock<IFileConnection> fileConnection = new Mock<IFileConnection>(
                                                           MockBehavior.Strict);
fileConnection.Setup(item => item.Get(It.IsAny<string>,It.IsAny<string>))
              .Throws(new IOException());

然后实例化您的 Transfer 类并在您的方法调用中使用模拟

Transfer transfer = new Transfer();
transfer.GetFile(fileConnection.Object, someRemoteFilename, someLocalFileName);

更新:

首先,您必须仅模拟您的依赖项,而不是您正在测试的类(在这种情况下为传输类)。在构造函数中声明这些依赖关系可以很容易地查看您的类需要哪些服务才能工作。当您编写单元测试时,它还可以用假货替换它们。目前不可能用假货替换这些属性。

由于您使用另一个依赖项设置这些属性,我会这样写:

public class Transfer
{
    public Transfer(IInternalConfig internalConfig)
    {
        source = internalConfig.GetFileConnection("source");
        destination = internalConfig.GetFileConnection("destination");
    }

    //you should consider making these private or protected fields
    public virtual IFileConnection source { get; set; }
    public virtual IFileConnection destination { get; set; }

    public virtual void GetFile(IFileConnection connection, 
        string remoteFilename, string localFilename)
    {
        connection.Get(remoteFilename, localFilename);
    }

    public virtual void PutFile(IFileConnection connection, 
        string localFilename, string remoteFilename)
    {
        connection.Get(remoteFilename, localFilename);
    }

    public virtual void TransferFiles(string sourceName, string destName)
    {
        var tempName = Path.GetTempFileName();
        GetFile(source, sourceName, tempName);
        PutFile(destination, tempName, destName);
    }
}

这样你就可以模拟 internalConfig 并让它返回你想要的 IFileConnection 模拟。

于 2012-04-25T21:16:05.950 回答
16

我认为这就是你想要的,我已经测试过这段代码并且可以工作

使用的工具有:(所有这些工具都可以作为 Nuget 包下载)

http://fluentassertions.codeplex.com/

http://autofixture.codeplex.com/

http://code.google.com/p/moq/

https://nuget.org/packages/AutoFixture.AutoMoq

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var myInterface = fixture.Freeze<Mock<IFileConnection>>();

var sut = fixture.CreateAnonymous<Transfer>();

myInterface.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>()))
        .Throws<System.IO.IOException>();

sut.Invoking(x => 
        x.TransferFiles(
            myInterface.Object, 
            It.IsAny<string>(), 
            It.IsAny<string>()
        ))
        .ShouldThrow<System.IO.IOException>();

编辑:

让我解释:

当你写一个测试时,你必须确切地知道你想测试什么,这被称为:“被测对象(SUT)”,如果我的理解是正确的,在这种情况下你的 SUT 是:Transfer

所以考虑到这一点,你不应该模拟你的 SUT,如果你替换你的 SUT,那么你就不会真正测试真正的代码

当您的 SUT 具有外部依赖项(非常常见)时,您需要替换它们以便单独测试的 SUT。当我说替代品时,我指的是根据您的需要使用模拟、虚拟、模拟等

在这种情况下,您的外部依赖项是IFileConnection您需要为此依赖项创建模拟并将其配置为抛出异常,然后只需调用您的 SUT 真实方法并断言您的方法按预期处理异常

  • var fixture = new Fixture().Customize(new AutoMoqCustomization());:这个 linie 初始化一个新的 Fixture 对象(Autofixture 库),这个对象用于创建 SUT,而不必显式地担心构造函数参数,因为它们是自动创建或模拟的,在这种情况下使用 Moq

  • var myInterface = fixture.Freeze<Mock<IFileConnection>>();: 这冻结了IFileConnection依赖。冻结意味着 Autofixture 在被询问时将始终使用此依赖项,为简单起见,就像单例一样。但有趣的是我们正在创建这个依赖的 Mock,你可以使用所有的 Moq 方法,因为这是一个简单的 Moq 对象

  • var sut = fixture.CreateAnonymous<Transfer>();:这里 AutoFixture 正在为我们创建 SUT

  • myInterface.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())).Throws<System.IO.IOException>();在这里,您将依赖项配置为在调用方法时抛出异常Get,此接口中的其余方法未配置,因此如果您尝试访问它们,您将收到意外异常

  • sut.Invoking(x => x.TransferFiles(myInterface.Object, It.IsAny<string>(), It.IsAny<string>())).ShouldThrow<System.IO.IOException>();:最后,是时候测试你的 SUT,这一行使用 FluenAssertions 库,它只是TransferFiles 从 SUT 调用真实方法,并作为参数接收模拟的参数,IFileConnection所以每当你在 SUT方法IFileConnection.Get的正常流程中调用TransferFiles模拟对象将调用抛出配置的异常,这是断言您的 SUT 正确处理异常的时候了,在这种情况下,我只是确保使用ShouldThrow<System.IO.IOException>()(来自 FluentAssertions 库)抛出异常

推荐参考:

http://martinfowler.com/articles/mocksArentStubs.html

http://misko.hevery.com/code-reviewers-guide/

http://misko.hevery.com/presentations/

http://www.youtube.com/watch?v=wEhu57pih5w&feature=player_embedded

http://www.youtube.com/watch?v=RlfLCWKxHJ0&feature=player_embedded

于 2012-04-25T23:51:49.590 回答
11

这就是我设法做我想做的事情的方式:

[Test]
public void TransferHandlesDisconnect()
{
    // ... set up config here
    var methodTester = new Mock<Transfer>(configInfo);
    methodTester.CallBase = true;
    methodTester
        .Setup(m => 
            m.GetFile(
                It.IsAny<IFileConnection>(), 
                It.IsAny<string>(), 
                It.IsAny<string>()
            ))
        .Throws<System.IO.IOException>();

    methodTester.Object.TransferFiles("foo1", "foo2");
    Assert.IsTrue(methodTester.Object.Status == TransferStatus.TransferInterrupted);
}

如果这种方法有问题,我想知道;其他答案表明我做错了,但这正是我想要做的。

于 2012-04-26T17:38:38.480 回答