web-dev-qa-db-fra.com

Se moquer d'une méthode pour lever une exception (moq), mais sinon agir comme l'objet moqué?

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?

47
Jeremy Holovacs

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.

9
Jeremy Holovacs

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.

65

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/

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

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

5
Jupaol