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.
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;
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
etStep 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.