Je me bats avec un problème de plus en plus ennuyeux concernant nos tests unitaires que nous mettons en œuvre dans mon équipe. Nous essayons d'ajouter des tests unitaires dans du code hérité qui n'était pas bien conçu et même si nous n'avons eu aucune difficulté avec l'ajout réel des tests, nous commençons à avoir du mal avec la façon dont les tests se déroulent.
Comme exemple du problème, supposons que vous ayez une méthode qui appelle 5 autres méthodes dans le cadre de son exécution. Un test pour cette méthode pourrait être de confirmer qu'un comportement se produit à la suite de l'appel de l'une de ces 5 autres méthodes. Donc, comme un test unitaire doit échouer pour une raison et une seule raison, vous voulez éliminer les problèmes potentiels causés par l'appel de ces 4 autres méthodes et les simuler. Génial! Le test unitaire s'exécute, les méthodes simulées sont ignorées (et leur comportement peut être confirmé dans le cadre d'autres tests unitaires), et la vérification fonctionne.
Mais il y a un nouveau problème - le test unitaire a une connaissance intime de la façon dont vous avez confirmé que le comportement et toute modification de signature à l'une de ces 4 autres méthodes à l'avenir, ou toute nouvelle méthode qui doit être ajoutée à la `` méthode parent '', sera oblige à modifier le test unitaire pour éviter d'éventuelles pannes.
Naturellement, le problème pourrait être quelque peu atténué en faisant simplement en sorte que plus de méthodes accomplissent moins de comportements, mais j'espérais qu'il y avait peut-être une solution plus élégante disponible.
Voici un exemple de test unitaire qui capture le problème.
Comme note rapide, «MergeTests» est une classe de tests unitaires qui hérite de la classe que nous testons et remplace le comportement au besoin. Il s'agit d'un «modèle» que nous utilisons dans nos tests pour nous permettre de remplacer les appels à des classes / dépendances externes.
[TestMethod]
public void VerifyMergeStopsSpinner()
{
var mockViewModel = new Mock<MergeTests> { CallBase = true };
var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());
mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
mockViewModel.Setup(
m =>
m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
It.IsAny<bool>()));
mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
mockViewModel.Setup(m => m.SwitchToOverviewTab());
mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));
mockViewModel.Object.OnMerge(It.IsAny<MergeState>());
mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}
Comment le reste d'entre vous avez-vous géré cela ou n'y a-t-il pas une excellente manière «simple» de le gérer?
Mise à jour - J'apprécie les commentaires de tout le monde. Malheureusement, et ce n'est pas vraiment une surprise, il ne semble pas y avoir une excellente solution, un modèle ou une pratique à suivre dans les tests unitaires si le code testé est médiocre. J'ai marqué la réponse qui captait le mieux cette simple vérité.
la source
Réponses:
Corrigez le code pour être mieux conçu. Si vos tests ont ces problèmes, votre code aura des problèmes plus graves lorsque vous essayez de changer les choses.
Si vous ne le pouvez pas, vous devez peut-être être moins idéal. Testez les pré et post-conditions de la méthode. Peu importe si vous utilisez les 5 autres méthodes? Ils ont vraisemblablement leurs propres tests unitaires expliquant clairement (er) ce qui a causé l'échec lorsque les tests échouent.
"Les tests unitaires ne devraient avoir qu'une seule raison d'échouer" est une bonne ligne directrice, mais d'après mon expérience, peu pratique. Les tests difficiles à écrire ne sont pas écrits. Les tests fragiles ne sont pas crus.
la source
Décomposer les grandes méthodes en petites méthodes plus ciblées est certainement une meilleure pratique. Vous voyez cela comme une douleur dans la vérification du comportement des tests unitaires, mais vous ressentez également la douleur d'autres manières.
Cela dit, c'est une hérésie mais je suis personnellement un fan de la création d'environnements de test temporaires réalistes. Autrement dit, plutôt que de vous moquer de tout ce qui est caché à l'intérieur de ces autres méthodes, assurez-vous qu'il existe un environnement temporaire facile à configurer (avec des bases de données et des schémas privés - SQLite peut vous aider ici) qui vous permet d'exécuter tout cela. La responsabilité de savoir comment créer / détruire cet environnement de test incombe au code qui l'exige, de sorte que lorsqu'il change, vous n'avez pas à modifier tout le code de test unitaire qui dépend de son existence.
Mais je note que c'est une hérésie de ma part. Les gens qui sont fortement dans les tests unitaires préconisent des tests unitaires "purs" et appellent ce que j'ai décrit des "tests d'intégration". Je ne m'inquiète pas personnellement de cette distinction.
la source
J'envisagerais d'assouplir les simulations et de formuler simplement des tests qui pourraient inclure les méthodes auxquelles il fait appel.
Ne testez pas le comment , testez le quoi . C'est le résultat qui compte, incluez les sous-méthodes si besoin est.
Sous un autre angle, vous pouvez formuler un test, le faire passer avec une grande méthode, refactoriser et vous retrouver avec un arbre de méthodes après refactoring. Vous n'avez pas besoin de tester chacun d'entre eux isolément. C'est le résultat final qui compte.
Si les sous-méthodes rendent difficile le test de certains aspects, envisagez de les répartir dans des classes distinctes afin de pouvoir les simuler plus proprement sans que votre classe testée soit fortement instrumentée / cousue. Il est difficile de dire si vous testez réellement une implémentation concrète dans votre exemple de test.
la source