J'ai une classe Transfer
, simplifiée, elle ressemble à ceci:
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);
}
}
La version simplifiée de l'interface IFileConnection
ressemble à ceci:
public interface IFileConnection
{
void Get(string remoteFileName, string localFileName);
void Put(string localFileName, string remoteFileName);
}
La vraie classe est censée gérer un System.IO.IOException
qui est levée lorsque les classes concrètes IFileConnection
perdent la connectivité avec la télécommande, envoyant des e-mails et ce qui ne l'est pas.
Je voudrais utiliser Moq pour créer une classe Transfer
et l'utiliser comme classe concrète Transfer
dans toutes les propriétés et méthodes, sauf lorsque la méthode GetFile
est invoquée - puis Je veux qu'il jette un System.IO.IOException
et assurez-vous que la classe Transfer
le gère correctement.
Suis-je en train d'utiliser le bon outil pour le travail? Est-ce que j'y vais de la bonne façon? Et comment pourrais-je écrire la configuration de ce test unitaire pour NUnit
?
Voici comment j'ai réussi à faire ce que j'essayais de faire:
[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);
}
S'il y a un problème avec cette méthode, j'aimerais savoir; les autres réponses suggèrent que je fais mal, mais c'est exactement ce que j'essayais de faire.
Voici comment vous pouvez vous moquer de votre FileConnection
Mock<IFileConnection> fileConnection = new Mock<IFileConnection>(
MockBehavior.Strict);
fileConnection.Setup(item => item.Get(It.IsAny<string>,It.IsAny<string>))
.Throws(new IOException());
Instanciez ensuite votre classe Transfer et utilisez la maquette dans votre appel de méthode
Transfer transfer = new Transfer();
transfer.GetFile(fileConnection.Object, someRemoteFilename, someLocalFileName);
Mise à jour:
Tout d'abord, vous devez vous moquer de vos dépendances uniquement, pas de la classe que vous testez (classe de transfert dans ce cas). Indiquer ces dépendances dans votre constructeur permet de voir facilement les services dont votre classe a besoin pour fonctionner. Il permet également de les remplacer par des faux lors de l'écriture de vos tests unitaires. Pour le moment, il est impossible de remplacer ces propriétés par des contrefaçons.
Puisque vous définissez ces propriétés en utilisant une autre dépendance, je l'écrirais comme ceci:
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);
}
}
De cette façon, vous pouvez vous moquer de internalConfig et lui faire retourner des simulations IFileConnection qui font ce que vous voulez.
Je pense que c'est ce que vous voulez, j'ai déjà testé ce code et ça marche
Les outils utilisés sont: (tous ces outils peuvent être téléchargés sous forme de packages Nuget)
http://fluentassertions.codeplex.com/
http://autofixture.codeplex.com/
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>();
Modifié:
Laissez-moi expliquer:
Lorsque vous écrivez un test, vous devez savoir exactement ce que vous voulez tester, cela s'appelle: "sujet sous test (SUT)", si ma compréhension est correcte, dans ce cas votre SUT est: Transfer
Donc, avec cela à l'esprit, vous ne devriez pas vous moquer de votre SUT, si vous remplacez votre SUT, vous ne testeriez pas réellement le vrai code
Lorsque votre SUT a des dépendances externes (très courantes), vous devez les remplacer afin de tester dans l'isolement de votre SUT. Quand je dis substitut, je fais référence à l'utilisation d'une maquette, d'un mannequin, d'une maquette, etc., selon vos besoins
Dans ce cas, votre dépendance externe est IFileConnection
, vous devez donc créer une simulation pour cette dépendance et la configurer pour lever l'exception, puis appelez simplement votre méthode réelle SUT et affirmez que votre méthode gère l'exception comme prévu
var fixture = new Fixture().Customize(new AutoMoqCustomization());
: Cette lignée initialise un nouvel objet Fixture (bibliothèque Autofixture), cet objet est utilisé pour créer des SUT sans avoir à se soucier explicitement des paramètres du constructeur, car ils sont créés automatiquement ou mockés, dans ce cas en utilisant Moq
var myInterface = fixture.Freeze<Mock<IFileConnection>>();
: Cela fige la dépendance IFileConnection
. Freeze signifie que Autofixture utilisera toujours cette dépendance lorsque demandé, comme un singleton pour plus de simplicité. Mais la partie intéressante est que nous créons une maquette de cette dépendance, vous pouvez utiliser toutes les méthodes Moq, car il s'agit d'un simple objet Moq
var sut = fixture.CreateAnonymous<Transfer>();
: Ici AutoFixture crée le SUT pour nous
myInterface.Setup(x => x.Get(It.IsAny<string>(), It.IsAny<string>())).Throws<System.IO.IOException>();
Ici, vous configurez la dépendance pour lever une exception à chaque appel de la méthode Get
, les autres méthodes de cette interface ne sont pas configurées, donc si vous essayez d'y accéder, vous recevra une exception inattendue
sut.Invoking(x => x.TransferFiles(myInterface.Object, It.IsAny<string>(), It.IsAny<string>())).ShouldThrow<System.IO.IOException>();
: Et enfin, le temps de tester votre SUT, cette ligne utilise la bibliothèque FluenAssertions, et elle appelle juste la vraie méthode TransferFiles
du SUT et en tant que paramètres, il reçoit le IFileConnection
simulé, donc chaque fois que vous appelez le IFileConnection.Get
dans le flux normal de votre méthode SUT TransferFiles
, le simulé objet invoquera la levée de l'exception configurée et c'est le moment d'affirmer que votre SUT gère correctement l'exception, dans ce cas, je m'assure simplement que l'exception a été levée en utilisant la ShouldThrow<System.IO.IOException>()
(à partir du Bibliothèque FluentAssertions)
Références recommandées:
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