AutoFixture / AutoMoq ignore l’instance injectée / fausse gelée

La conclusion à retenir maintenant que la solution a été trouvée:

AutoFixture retourne la maquette figée, très bien; mon sut qui a également été généré par AutoFixture a juste une propriété publique avec un défaut local qui était important pour le test et que AutoFixture a défini sur une nouvelle valeur. Il y a beaucoup à apprendre au-delà de cela de la réponse de Mark.

Question originale:

J’ai commencé à essayer AutoFixture hier pour mes tests xUnit.net qui ont Moq sur eux. J’espérais remplacer certains éléments de Moq ou faciliter la lecture, et je suis particulièrement intéressé par l’utilisation de la fonction AutoFixture en tant qu’utilitaire SUT Factory.

Je me suis armé de quelques articles de blog de Mark Seemann sur AutoMocking et j’ai essayé de travailler à partir de là, mais je ne suis pas allé très loin.

Voici à quoi ressemblait mon test sans la correction automatique:

[Fact] public void GetXml_ReturnsCorrectXElement() { // Arrange ssortingng xmlSsortingng = @"    "; ssortingng settingKey = "gcCreditApplicationUsdFieldMappings"; Mock settingsMock = new Mock(); settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlSsortingng); ISettings settings = settingsMock.Object; ITracingService tracing = new Mock().Object; XElement expectedXml = XElement.Parse(xmlSsortingng); 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 clé correcte (qui est codée en dur / une propriété injectée) et renvoie le résultat sous la forme d’un XElement . ITracingService n’est pertinent qu’en cas d’erreur.

Ce que j’essayais de faire, c’est d’éliminer le besoin de créer explicitement l’object ITracingService , puis d’injecter manuellement les dépendances (non pas parce que ce test est trop complexe, mais parce qu’il est assez simple pour essayer et comprendre les choses).

Entrez AutoFixture – première tentative:

 [Fact] public void GetXml_ReturnsCorrectXElement() { // Arrange IFixture fixture = new Fixture(); fixture.Customize(new AutoMoqCustomization()); ssortingng xmlSsortingng = @"    "; ssortingng settingKey = "gcCreditApplicationUsdFieldMappings"; Mock settingsMock = new Mock(); settingsMock.Setup(s => s.Get(settingKey)).Returns(xmlSsortingng); ISettings settings = settingsMock.Object; fixture.Inject(settings); XElement expectedXml = XElement.Parse(xmlSsortingng); IMappingXml sut = fixture.CreateAnonymous(); // Act XElement actualXml = sut.GetXml(); // Assert Assert.True(XNode.DeepEquals(expectedXml, actualXml)); } 

CreateAnonymous() de la détection du paramètre de constructeur ISettings , CreateAnonymous() remarquerait qu’une instance concrète avait été enregistrée pour cette interface et l’injectait. Toutefois, il ne le fait pas mais crée une nouvelle implémentation anonyme.

Ceci est particulièrement déroutant car fixture.CreateAnonymous() renvoie effectivement mon instance –

 IMappingXml sut = new SettingMappingXml(fixture.CreateAnonymous(), fixture.CreateAnonymous()); 

rend le test parfaitement vert et cette ligne correspond exactement à ce que je pensais que AutoFixture ferait en interne lors de l’instanciation de SettingMappingXml .

Puis il y a le concept de geler un composant, alors je suis allé de l’avant, geler la maquette dans le gabarit au lieu de récupérer l’object simulé:

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

Cela fonctionne parfaitement, tant que j’appelle explicitement le constructeur SettingMappingXml et que je ne compte pas sur CreateAnonymous() .

En termes simples, je ne comprends pas pourquoi cela fonctionne comme cela semble, car cela va à l’encontre de toute logique que je puisse imaginer. Normalement, je suspecterais un bogue dans la bibliothèque, mais c’est quelque chose de tellement fondamental que je suis sûr que d’autres se seraient heurtés à ce problème et que cela aurait été longtemps trouvé et corrigé. De plus, connaissant l’approche assidue de Mark en matière de test et de DI, cela ne peut ê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 object fictif préconfiguré en tant que dépendance? La seule chose dont je suis sûr à l’heure actuelle, c’est que j’ai besoin d’ AutoMoqCustomization pour ne pas avoir à configurer quoi que ce soit pour ITracingService .

Les packages AutoFixture / AutoMoq sont 2.14.1, Moq est 3.1.416.3, tous de NuGet. La version .NET est 4.5 (installée avec VS2012), le comportement est identique dans VS2012 et 2010.

En écrivant ce billet, j’ai découvert que certaines personnes rencontraient des problèmes avec Moq 4.0 et les redirections de liaison d’assemblage. J’ai donc purifié méticuleusement ma solution de toutes les instances de Moq 4 et j’ai installé Moq 3.1 en installant AutoFixture.AutoMoq dans des projets “propres”. Cependant, le comportement de mon test rest inchangé.

Merci pour tous les pointeurs et explications.

Mise à jour: Voici le code constructeur demandé par Mark:

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

Et pour être complet, la méthode GetXml() ressemble à ceci:

 public XElement GetXml() { int errorCode = 10600; try { ssortingng 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 juste une propriété automatique.

En supposant que la propriété SettingKey soit définie comme suit, je peux maintenant reproduire le problème:

 public ssortingng SettingKey { get; set; } 

En réalité, les doublons de test injectés dans l’instance SettingMappingXml conviennent parfaitement. Cependant, comme SettingKey est accessible en écriture, la fonction Auto-Properties d’AutoFixture Auto-Properties active et modifie la valeur.

Considérons ce code:

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

Cela affiche quelque chose comme ceci:

ParamétrageKey83b75965-2886-4308-bcc4-eb0f8e63de09

Même si tous les doublons de test sont correctement injectés, l’attente de la méthode d’ Setup n’est pas remplie.

Il existe de nombreuses façons de résoudre ce problème.

Protéger les invariants

La bonne façon de résoudre ce problème consiste à utiliser le test unitaire et la correction automatique comme mécanisme de retour. C’est l’un des points essentiels du GOOS : les problèmes liés aux tests unitaires sont souvent le symptôme d’une faille de conception plutôt que la faute du test unitaire (ou de la fixation automatique) lui-même.

Dans ce cas, cela m’indique que la conception n’est pas assez solide . Est-il vraiment approprié qu’un client puisse manipuler la SettingKey à sa SettingKey ?

Au minimum, je recommanderais une implémentation alternative comme celle-ci:

 public ssortingng SettingKey { get; private set; } 

Avec ce changement, mon repro passe.

Omettre SettingKey

Si vous ne pouvez pas (ou ne voudrez pas) changer votre conception, vous pouvez indiquer à AutoFixture de ne pas définir la propriété SettingKey :

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

Personnellement, je trouve contre-productif de devoir écrire une expression Build chaque fois que j’ai besoin d’une instance d’une classe particulière. Vous pouvez découpler la SettingMappingXml instance SettingMappingXml partir de l’instanciation réelle:

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

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( c => c.Without(s => s.SettingKey)); } } 

Cela nécessite que vous créiez votre instance Fixture avec cette personnalisation:

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

Une fois que vous avez plus de deux ou trois personnalisations à chaîner, vous risquez de vous lasser d’écrire cette chaîne de méthodes 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()); 

TestConventions vous offre un emplacement central où vous pouvez modifier vos conventions pour la suite de tests, lorsque vous en avez besoin. Il réduit la taxe de maintenabilité de vos tests unitaires et consortingbue à la cohérence de la conception de votre code de production.

Enfin, comme il semble que vous utilisiez xUnit.net, vous pouvez utiliser l’intégration xUnit.net d’AutoFixture , mais vous devez utiliser un style moins impératif de manipulation du Fixture . Il s’avère que le code qui crée, configure et injecte ISettings Test Double est si idiomatique qu’il dispose d’un raccourci appelé Freeze :

 fixture.Freeze>() .Setup(s => s.Get(settingKey)).Returns(xmlSsortingng); 

Cela fait, l’étape suivante consiste à définir un AutoDataAtsortingbute personnalisé:

 public class AutoConventionDataAtsortingbute : AutoDataAtsortingbute { public AutoConventionDataAtsortingbute() : base(new Fixture().Customize(new TestConventions())) { } } 

Vous pouvez maintenant réduire le test à l’essentiel, en éliminant tout le bruit, en permettant au test d’exprimer uniquement ce qui compte:

 [Theory, AutoConventionData] public void ReducedTheory( [Frozen]Mock settingsStub, SettingMappingXml sut) { ssortingng xmlSsortingng = @"    "; ssortingng settingKey = "gcCreditApplicationUsdFieldMappings"; settingsStub.Setup(s => s.Get(settingKey)).Returns(xmlSsortingng); XElement actualXml = sut.GetXml(); XElement expectedXml = XElement.Parse(xmlSsortingng); Assert.True(XNode.DeepEquals(expectedXml, actualXml)); } 

Autres options

Pour que le test initial réussisse, vous pouvez également simplement désactiver les propriétés automatiques:

 fixture.OmitAutoProperties = true; 

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

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

Ensuite, les seuls changements sont les suivants:

Étape 1

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

Étape 2

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

Étape 3

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

C’est tout!


Voici comment cela fonctionne:

En interne, Freeze crée une instance du type demandé (par exemple, Mock ), puis l’ injecte de manière à ce qu’elle retourne toujours cette instance lorsque vous le demandez à nouveau.

C’est ce que nous faisons aux Step 1 et Step 2 .

À l’ Step 3 nous demandons une instance du type SettingMappingXml qui dépend de ISettings et ITracingService . Comme nous utilisons Auto Mocking, la classe Fixture fournira des modèles pour ces interfaces. Cependant, nous leur avons précédemment injecté Freeze afin que les Freeze déjà créés soient automatiquement fournis.