AutoFixture / AutoMoq ne tient pas compte de l'instance injectée / de la maquette congelée

le bref maintenant que la solution a été trouvée:

AutoFixture retourne gelé la simulation de l'amende juste; mon sut que a également été généré par AutoFixture juste eu un bien public local par défaut c'est important pour le test et que AutoFixture définir une nouvelle valeur. Il y a beaucoup à apprendre de la réponse de Mark.

question originale:

j'ai commencé à essayer AutoFixture hier pour mon xUnit.net des tests qui ont du Moq partout. J'espérais remplacer certaines choses du Moq ou les rendre plus faciles à lire, et je suis particulièrement intéressé à utiliser L'AutoFixture dans la capacité de L'usine de SUT.

je me suis armé avec quelques-uns des billets de Mark Seemann sur le blog AutoMocking et j'ai essayé de travailler à partir de là, mais je n'ai pas été très loin.

c'est à ça que ressemblait mon test sans AutoFixture:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
    // Arrange
    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";

    string settingKey = "gcCreditApplicationUsdFieldMappings";

    Mock<ISettings> settingsMock = new Mock<ISettings>();
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
    ISettings settings = settingsMock.Object;

    ITracingService tracing = new Mock<ITracingService>().Object;

    XElement expectedXml = XElement.Parse(xmlString);

    IMappingXml sut = new SettingMappingXml(settings, tracing);

    // Act
    XElement actualXml = sut.GetXml();

    // Assert
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

l'histoire ici est assez simple - assurez-vous que SettingMappingXml interroge la dépendance ISettings avec la bonne clé (qui est codée dur/propriété injectée) et renvoie le résultat comme un XElement . Le ITracingService n'est pertinent que s'il y a une erreur.

ce que j'ai essayé de faire est de me débarrasser de la nécessité de créer explicitement l'objet ITracingService et puis d'injecter manuellement les dépendances (pas parce que ce test est trop complexe, mais parce qu'il est assez simple pour essayer des choses et de les comprendre).

Entrez AutoFixture - première tentative:

[Fact]
public void GetXml_ReturnsCorrectXElement()
{
    // Arrange
    IFixture fixture = new Fixture();
    fixture.Customize(new AutoMoqCustomization());

    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";

    string settingKey = "gcCreditApplicationUsdFieldMappings";

    Mock<ISettings> settingsMock = new Mock<ISettings>();
    settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlString);
    ISettings settings = settingsMock.Object;
    fixture.Inject(settings);

    XElement expectedXml = XElement.Parse(xmlString);

    IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

    // Act
    XElement actualXml = sut.GetXml();

    // Assert
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

Je m'attendrais à ce que CreateAnonymous<SettingMappingXml>() , après la détection du paramètre ISettings constructeur, de noter qu'une instance concrète a été enregistrée pour cette interface et d'injecter que - cependant, il ne fait pas cela, mais crée à la place une nouvelle implémentation anonyme.

C'est d'autant plus déroutant que fixture.CreateAnonymous<ISettings>() renvoie effectivement mon instance -

IMappingXml sut = new SettingMappingXml(fixture.CreateAnonymous<ISettings>(), fixture.CreateAnonymous<ITracingService>());

rend le test parfaitement vert, et cette ligne est exactement ce que j'avais prévu AutoFixture à faire en interne lors de l'instanciation SettingMappingXml .

puis il y a le concept de congélation d'un composant, donc j'ai procédé en gelant la moquerie dans le montage au lieu d'obtenir l'objet moqué:

fixture.Freeze<Mock<ISettings>>(f => f.Do(m => m.Setup(s => s.Get(settingKey)).Returns(xmlString)));

bien Sûr ce fonctionne parfaitement bien-aussi longtemps que j'appelle le constructeur SettingMappingXml explicitement et ne pas compter sur CreateAnonymous() .





Pour dire les choses simplement, Je ne comprends pas pourquoi cela fonctionne de la façon dont cela fonctionne apparemment, car cela va à l'encontre de toute logique que je peux évoquer. Normalement, je suspecterais un bug dans la bibliothèque, mais c'est quelque chose de si basique que je suis sûr que d'autres l'auraient trouvé et corrigé depuis longtemps. Ce qui est de plus, connaissant L'approche assidue de Mark pour les tests et L'ai, cela ne peut pas être involontaire.

cela signifie que je dois manquer quelque chose d'assez élémentaire. Comment puis-je avoir mon SUT créé par AutoFixture avec un objet préconfiguré moqué comme une dépendance? La seule chose dont je suis sûr pour le moment est que j'ai besoin du AutoMoqCustomization pour ne pas avoir à configurer quoi que ce soit pour le ITracingService .

les paquets AutoFixture/AutoMoq sont 2.14.1, Moq est 3.1.416.3, tous de NuGet. La version. net est 4.5 (installé avec VS2012), le comportement est le même dans VS2012 et 2010.

en écrivant ce post, j'ai découvert que certaines personnes avaient des problèmes avec le Moq 4.0 et l'assemblage liant redirections, donc j'ai méticuleusement purgé ma solution de toutes les instances de Moq 4 et fait installer Moq 3.1 en installant AutoFixture.AutoMoq dans les projets "propres". Toutefois, le comportement de mon test reste inchangé.

Merci vous les pointeurs et les explications.

mise à jour: voici le code de constructeur demandé pour:

public SettingMappingXml(ISettings settingSource, ITracingService tracing)
{
    this._settingSource = settingSource;
    this._tracing = tracing;

    this.SettingKey = "gcCreditApplicationUsdFieldMappings";
}

et pour être complète, la méthode GetXml() ressemble à ceci:

public XElement GetXml()
{
    int errorCode = 10600;

    try
    {
        string mappingSetting = this._settingSource.Get(this.SettingKey);
        errorCode++;

        XElement mappingXml = XElement.Parse(mappingSetting);
        errorCode++;

        return mappingXml;
    }
    catch (Exception e)
    {
        this._tracing.Trace(errorCode, e.Message);
        throw;
    }
}

SettingKey est une propriété automatique.

9
demandé sur TeaDrivenDev 2012-11-21 04:21:41

2 réponses

en supposant que la propriété SettingKey est définie comme suit, je peux maintenant reproduire la question:

public string SettingKey { get; set; }

ce qui se passe, c'est que le test Double injecté dans L'instance de SettingMappingXml est parfaitement correct, mais parce que le SettingKey est inscriptible, la fonction Auto-properties d'AutoFixture s'active et modifie la valeur.

considérez ce code:

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var sut = fixture.CreateAnonymous<SettingMappingXml>();
Console.WriteLine(sut.SettingKey);

This imprime quelque chose comme ceci:

SettingKey83b75965-2886-4308-cbc4-eb0f8e63de09

même si tous les Doubles D'essai sont injectés correctement, l'exigence de la méthode Setup n'est pas respectée.

il y a plusieurs façons d'aborder cette question.

protéger les invariants

La bonne façon de résoudre ce problème est de utilisez le test de l'unité et L'AutoFixture comme mécanisme de rétroaction. C'est l'un des points clés de GOOS : les problèmes avec les tests unitaires sont souvent un symptôme d'un défaut de conception plutôt que la faute du test unitaire (ou AutoFixture) lui-même.

dans ce cas, il m'indique que le dessin n'est pas assez infaillible . Est-il vraiment approprié qu'un client puisse manipuler le SettingKey à volonté?

As un strict minimum, je recommanderais une implémentation alternative comme celle-ci:

public string SettingKey { get; private set; }

Avec ce changement, ma repro passe.

Omettre SettingKey

si vous ne pouvez pas (ou ne voulez pas) changer votre design, vous pouvez demander à AutoFixture de sauter le paramétrage de la propriété SettingKey :

IMappingXml sut = fixture
    .Build<SettingMappingXml>()
    .Without(s => s.SettingKey)
    .CreateAnonymous();

personnellement, je trouve improductif d'avoir à écrire un Build expression chaque fois que j'ai besoin d'une instance d'une classe particulière. Vous pouvez découpler la façon dont l'instance SettingMappingXml est créée à partir de l'instance actuelle:

fixture.Customize<SettingMappingXml>(
    c => c.Without(s => s.SettingKey));
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

pour aller plus loin, vous pouvez encapsuler cet appel de méthode Customize dans une personnalisation .

public class SettingMappingXmlCustomization : ICustomization
{
    public void Customize(IFixture fixture)
    {
        fixture.Customize<SettingMappingXml>(
            c => c.Without(s => s.SettingKey));
    }
}

cela vous oblige à créer votre instance Fixture avec cette personnalisation:

IFixture fixture = new Fixture()
    .Customize(new SettingMappingXmlCustomization())
    .Customize(new AutoMoqCustomization());

une fois vous obtenez plus de deux ou trois personnalisations à la chaîne, vous pouvez vous fatiguer d'écrire cette chaîne de méthode tout le temps. Il est temps d'encapsuler ces personnalisations dans un ensemble de conventions pour votre bibliothèque particulière:

public class TestConventions : CompositeCustomization
{
    public TestConventions()
        : base(
            new SettingMappingXmlCustomization(),
            new AutoMoqCustomization())
    {
    }
}

cela vous permet de toujours créer l'instance Fixture comme ceci:

IFixture fixture = new Fixture().Customize(new TestConventions());

le TestConventions vous donne un endroit central où vous pouvez aller et modifier occasionnellement vos conventions pour le test suite lorsque vous avez besoin de le faire. Il réduit la taxe de maintenabilité de vos tests unitaires et aide à garder la conception de votre code de production plus cohérente.

enfin, puisqu'il semble que vous utilisez xUnit.net, vous pouvez utiliser AutoFixture xUnit.net intégration , mais avant de faire que vous auriez besoin d'utiliser un style moins impératif de manipuler le Fixture . Il s'avère que le code qui crée, configure et injecte ISettings Test Double est si idiomatique qu'il a un raccourci appelé Freeze :

fixture.Freeze<Mock<ISettings>>()
    .Setup(s => s.Get(settingKey)).Returns(xmlString);

avec cela en place, l'étape suivante est de définir un AutoDataAttribute personnalisé:

public class AutoConventionDataAttribute : AutoDataAttribute
{
    public AutoConventionDataAttribute()
        : base(new Fixture().Customize(new TestConventions()))
    {
    }
}

vous pouvez maintenant réduire le test à l'essentiel, se débarrasser de tout le bruit, permettant au test d'exprimer succinctement seulement ce qui importe:

[Theory, AutoConventionData]
public void ReducedTheory(
    [Frozen]Mock<ISettings> settingsStub,
    SettingMappingXml sut)
{
    string xmlString = @"
        <mappings>
            <mapping source='gcnm_loan_amount_min' target='gcnm_loan_amount_min_usd' />
            <mapping source='gcnm_loan_amount_max' target='gcnm_loan_amount_max_usd' />
        </mappings>";
    string settingKey = "gcCreditApplicationUsdFieldMappings";
    settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlString);

    XElement actualXml = sut.GetXml();

    XElement expectedXml = XElement.Parse(xmlString);
    Assert.True(XNode.DeepEquals(expectedXml, actualXml));
}

autres options

pour faire passer le test d'origine, vous pouvez également juste désactiver Auto-propriétés entièrement:

fixture.OmitAutoProperties = true;
13
répondu Mark Seemann 2012-11-22 09:22:22

dans le premier test vous pouvez créer une instance de la classe Fixture avec le AutoMoqCustomization appliqué:

var fixture = new Fixture()
    .Customize(new AutoMoqCustomization());

alors, les seuls changements sont:

Étape 1

// The following line:
Mock<ISettings> settingsMock = new Mock<ISettings>();
// Becomes:
Mock<ISettings> settingsMock = fixture.Freeze<Mock<ISettings>>();

Étape 2

// The following line:
ITracingService tracing = new Mock<ITracingService>().Object;
// Becomes:
ITracingService tracing = fixture.Freeze<Mock<ITracingService>>().Object;

Étape 3

// The following line:
IMappingXml sut = new SettingMappingXml(settings, tracing);
// Becomes:
IMappingXml sut = fixture.CreateAnonymous<SettingMappingXml>();

C'est ça!


Voici comment ça marche:

à L'interne, Freeze crée une instance du type demandé (par exemple Mock<ITracingService> ) puis l'injecte il ainsi il retournera toujours cette instance lorsque vous la Demandez à nouveau.

C'est ce que nous faisons dans Step 1 et Step 2 .

dans Step 3 nous demandons une instance du type SettingMappingXml qui dépend de ISettings et ITracingService . Puisque nous utilisons L'Auto Mocking, la classe Fixture fournira des mocks pour ces interfaces. Cependant, nous avons précédemment injecté eux avec Freeze de sorte que les moqueurs déjà créés sont maintenant automatiquement fournis.

4
répondu Nikos Baxevanis 2012-11-21 02:50:43