模拟方法以抛出异常(moq) ,但是在其他方面却像模拟对象一样?

我有一个 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.IOException,当 IFileConnection具体类与远程失去连接,发送电子邮件等等时抛出。

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

我用对工具了吗?我这么做对吗?我该如何编写 NUnit的单元测试设置?

154460 次浏览

你可以这样嘲笑你的 FileConnection

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

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

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);
}
}

通过这种方式,您可以模拟 interalConfig 并使其返回 IFileConnection 模拟,这些模拟可以完成您想要的任务。

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

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

Http://fluentassertions.codeplex.com/

Http://autofixture.codeplex.com/

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

Https://nuget.org/packages/autofixture

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>();

编辑:

让我解释一下:

当您编写测试时,您必须确切地知道您想要测试什么,这被称为: “ subject under test (SUT)”,如果我的理解正确的话,在这种情况下,您的 SUT 是: Transfer

因此,考虑到这一点,您不应该模仿 SUT,如果您替换 SUT,那么您实际上就不会测试真正的代码

当您的 SUT 具有外部依赖项(非常常见)时,您需要替换它们,以便在 与世隔绝中测试您的 SUT。当我说“替代品”时,我指的是根据你的需要使用模仿、假人、模仿等

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

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

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

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

  • 在这里,你配置依赖项,以便在调用 Get方法时抛出一个异常,这个接口的其余方法没有配置,因此,如果你尝试访问它们,你会得到一个意外的异常

  • sut.Invoking(x => x.TransferFiles(myInterface.Object, It.IsAny<string>(), It.IsAny<string>())).ShouldThrow<System.IO.IOException>();: 最后,测试 SUT 的时间,这一行使用 FluenAssertions 库,它只调用 TransferFiles 来自 SUT 的真正方法,作为参数它接收模拟 IFileConnection,所以当你在 SUT TransferFiles方法的正常流程中调用 IFileConnection.Get时,模拟对象将调用抛出配置的异常,这是断言您的 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

这就是我如何设法做到我想做的事情:

[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);
}

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