可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have a Transfer
class, simplified it looks like this:
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); } }
The simplified version of the IFileConnection
interface looks like this:
public interface IFileConnection { void Get(string remoteFileName, string localFileName); void Put(string localFileName, string remoteFileName); }
The real class is supposed to handle a System.IO.IOException
that is thrown when the IFileConnection
concrete classes loses connectivity with the remote, sending out emails and what not.
I would like to use Moq to create a Transfer
class, and use it as my concrete Transfer
class in all properties and methods, except when the GetFile
method is invoked - then I want it to throw a System.IO.IOException
and make sure the Transfer
class handles it properly.
Am I using the right tool for the job? Am I going about this the right way? And how would I write the setup for that unit test for NUnit
?
回答1:
Here's how you can mock your FileConnection
Mock<IFileConnection> fileConnection = new Mock<IFileConnection>( MockBehavior.Strict); fileConnection.Setup(item => item.Get(It.IsAny<string>,It.IsAny<string>)) .Throws(new IOException());
Then instantiate your Transfer class and use the mock in your method call
Transfer transfer = new Transfer(); transfer.GetFile(fileConnection.Object, someRemoteFilename, someLocalFileName);
Update:
First of all you have to mock your dependencies only, not the class you are testing(Transfer class in this case). Stating those dependencies in your constructor make it easy to see what services your class needs to work. It also makes it possible to replace them with fakes when you are writing your unit tests. At the moment it's impossible to replace those properties with fakes.
Since you are setting those properties using another dependency, I would write it like this:
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); } }
This way you can mock internalConfig and make it return IFileConnection mocks that does what you want.
回答2:
This is how I managed to do what I was trying to do:
[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); }
If there is a problem with this method, I would like to know; the other answers suggest I am doing this wrong, but this was exactly what I was trying to do.
回答3:
I think this is what you want, I already tested this code and works
The tools used are: (all these tools can be downloaded as Nuget packages)
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>();
Edited:
Let me explain:
When you write a test, you must know exactly what you want to test, this is called: "subject under test (SUT)", if my understanding is correctly, in this case your SUT is: Transfer
So with this in mind, you should not mock your SUT, if you substitute your SUT, then you wouldn't be actually testing the real code
When your SUT has external dependencies (very common) then you need to substitute them in order to test in isolation your SUT. When I say substitute I'm referring to use a mock, dummy, mock, etc depending on your needs
In this case your external dependency is IFileConnection
so you need to create mock for this dependency and configure it to throw the exception, then just call your SUT real method and assert your method handles the exception as expected
var fixture = new Fixture().Customize(new AutoMoqCustomization());
: This linie initializes a new Fixture object (Autofixture library), this object is used to create SUT's without having to explicitly have to worry about the constructor parameters, since they are created automatically or mocked, in this case using Moq
var myInterface = fixture.Freeze<Mock<IFileConnection>>();
: This freezes the IFileConnection
dependency. Freeze means that Autofixture will use always this dependency when asked, like a singleton for simplicity. But the interesting part is that we are creating a Mock of this dependency, you can use all the Moq methods, since this is a simple Moq object
var sut = fixture.CreateAnonymous<Transfer>();
: Here AutoFixture is creating the SUT for us
myInterface.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())).Throws<System.IO.IOException>();
Here you are configuring the dependency to throw an exception whenever the Get
method is called, the rest of the methods from this interface are not being configured, therefore if you try to access them you will get an unexpected exception
sut.Invoking(x => x.TransferFiles(myInterface.Object, It.IsAny<string>(), It.IsAny<string>())).ShouldThrow<System.IO.IOException>();
: And finally, the time to test your SUT, this line uses the FluenAssertions library, and it just calls the TransferFiles
real method from the SUT and as parameters it receives the mocked IFileConnection
so whenever you call the IFileConnection.Get
in the normal flow of your SUT TransferFiles
method, the mocked object will be invoking throwing the configured exception and this is the time to assert that your SUT is handling correctly the exception, in this case, I am just assuring that the exception was thrown by using the ShouldThrow<System.IO.IOException>()
(from the FluentAssertions library)
References recommended:
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