Afin d'aider mon équipe à écrire du code testable, j'ai élaboré cette simple liste de bonnes pratiques pour rendre notre base de code C # plus testable. (Certains points font référence aux limitations de Rhino Mocks, un cadre de simulation pour C #, mais les règles peuvent également s'appliquer plus généralement.) Quelqu'un a-t-il des bonnes pratiques à suivre?
Pour maximiser la testabilité du code, suivez ces règles:
Écrivez d'abord le test, puis le code. Raison: Cela garantit que vous écrivez du code testable et que chaque ligne de code reçoit des tests écrits.
Concevez des classes à l'aide de l'injection de dépendances. Raison: vous ne pouvez pas vous moquer ou tester ce qui ne peut pas être vu.
Séparez le code d'interface utilisateur de son comportement à l'aide de Model-View-Controller ou Model-View-Presenter. Raison: permet de tester la logique métier tandis que les parties qui ne peuvent pas être testées (l'interface utilisateur) sont minimisées.
N'écrivez pas de méthodes ou de classes statiques. Raison: Les méthodes statiques sont difficiles ou impossibles à isoler et Rhino Mocks est incapable de se moquer d'elles.
Programmez des interfaces, pas des classes. Raison: l'utilisation d'interfaces clarifie les relations entre les objets. Une interface doit définir un service dont un objet a besoin à partir de son environnement. En outre, les interfaces peuvent être facilement simulées à l'aide de Rhino Mocks et d'autres frameworks moqueurs.
Isolez les dépendances externes. Raison: les dépendances externes non résolues ne peuvent pas être testées.
Marquez comme virtuelles les méthodes dont vous avez l'intention de vous moquer. Raison: Rhino Mocks ne peut pas simuler des méthodes non virtuelles.
la source
Réponses:
Certainement une bonne liste. Voici quelques réflexions à ce sujet:
Je suis d'accord, à un niveau élevé. Mais, je serais plus précis: "Écrivez d'abord un test, puis écrivez juste assez de code pour réussir le test et répétez." Sinon, j'aurais peur que mes tests unitaires ressemblent davantage à des tests d'intégration ou d'acceptation.
D'accord. Lorsqu'un objet crée ses propres dépendances, vous n'avez aucun contrôle sur celles-ci. Inversion of Control / Dependency Injection vous donne ce contrôle, vous permettant d'isoler l'objet sous test avec mocks / stubs / etc. C'est ainsi que vous testez des objets de manière isolée.
D'accord. Notez que même le présentateur / contrôleur peut être testé à l'aide de DI / IoC, en lui remettant une vue et un modèle stubbed / simulé. Consultez Presenter First TDD pour en savoir plus.
Je ne suis pas sûr d'être d'accord avec celui-ci. Il est possible d'effectuer des tests unitaires sur une méthode / classe statique sans utiliser de simulation. Alors, c'est peut-être l'une de ces règles spécifiques à Rhino Mock que vous avez mentionnées.
Je suis d'accord, mais pour une raison légèrement différente. Les interfaces offrent une grande flexibilité au développeur de logiciels - au-delà de la simple prise en charge de divers frameworks d'objets fictifs. Par exemple, il n'est pas possible de prendre en charge correctement DI sans interfaces.
D'accord. Cachez les dépendances externes derrière votre propre façade ou adaptateur (selon le cas) avec une interface. Cela vous permettra d'isoler votre logiciel de la dépendance externe, que ce soit un service Web, une file d'attente, une base de données ou autre. Ceci est particulièrement important lorsque votre équipe ne contrôle pas la dépendance (aka externe).
C'est une limitation de Rhino Mocks. Dans un environnement qui préfère les stubs codés à la main à un framework d'objet fictif, cela ne serait pas nécessaire.
Et, quelques nouveaux points à considérer:
Utilisez des modèles de conception créatifs. Cela vous aidera avec DI, mais cela vous permettra également d'isoler ce code et de le tester indépendamment de toute autre logique.
Rédiger des tests en utilisant la technique Arrange / Act / Assert de Bill Wake .Cette technique indique très clairement quelle configuration est nécessaire, ce qui est réellement testé et ce qui est attendu.
N'ayez pas peur de rouler vos propres mocks / stubs. Souvent, vous constaterez que l'utilisation de cadres d'objet simulés rend vos tests incroyablement difficiles à lire. En lançant le vôtre, vous aurez un contrôle total sur vos simulacres / stubs, et vous serez en mesure de garder vos tests lisibles. (Reportez-vous au point précédent.)
Évitez la tentation de refactoriser la duplication de vos tests unitaires vers des classes de base abstraites ou des méthodes de configuration / suppression. Cela masque le code de configuration / nettoyage du développeur qui tente de réaliser le test unitaire. Dans ce cas, la clarté de chaque test individuel est plus importante que la refactorisation de la duplication.
Mettre en œuvre l'intégration continue. Enregistrez votre code sur chaque «barre verte». Créez votre logiciel et exécutez votre suite complète de tests unitaires à chaque enregistrement. (Bien sûr, ce n'est pas une pratique de codage en soi; mais c'est un outil incroyable pour garder votre logiciel propre et entièrement intégré.)
la source
Si vous travaillez avec .Net 3.5, vous pouvez consulter la bibliothèque Moq mocking - elle utilise des arborescences d'expression et des lambdas pour supprimer les idiomes non intuitifs de réponse d'enregistrement de la plupart des autres bibliothèques moqueuses.
Consultez ce guide de démarrage rapide pour voir à quel point vos cas de test deviennent plus intuitifs, voici un exemple simple:
// ShouldExpectMethodCallWithVariable int value = 5; var mock = new Mock<IFoo>(); mock.Expect(x => x.Duplicate(value)).Returns(() => value * 2); Assert.AreEqual(value * 2, mock.Object.Duplicate(value));
la source
Connaître la différence entre les faux, les simulacres et les talons et quand les utiliser.
Évitez de trop spécifier les interactions en utilisant des simulations. Cela rend les tests fragiles .
la source
Ceci est un article très utile!
J'ajouterais qu'il est toujours important de comprendre le contexte et le système sous test (SUT). Suivre les principaux TDD à la lettre est beaucoup plus facile lorsque vous écrivez un nouveau code dans un environnement où le code existant suit les mêmes principaux. Mais lorsque vous écrivez un nouveau code dans un environnement hérité non TDD, vous constatez que vos efforts TDD peuvent rapidement dépasser vos estimations et vos attentes.
Pour certains d'entre vous, qui vivent dans un monde entièrement académique, les délais et la livraison peuvent ne pas être importants, mais dans un environnement où les logiciels sont de l'argent, il est essentiel d'utiliser efficacement vos efforts de TDD.
Le TDD est fortement soumis à la loi du rendement marginal décroissant . En bref, vos efforts en faveur du TDD sont de plus en plus précieux jusqu'à ce que vous atteigniez un point de rendement maximal, après quoi le temps investi dans le TDD a de moins en moins de valeur.
J'ai tendance à croire que la valeur principale de TDD réside dans les limites (boîte noire) ainsi que dans les tests occasionnels en boîte blanche des zones critiques du système.
la source
La vraie raison de programmer contre des interfaces n'est pas de faciliter la vie de Rhino, mais de clarifier les relations entre les objets dans le code. Une interface doit définir un service dont un objet a besoin à partir de son environnement. Une classe fournit une implémentation particulière de ce service. Lisez le livre "Object Design" de Rebecca Wirfs-Brock sur les rôles, les responsabilités et les collaborateurs.
la source
Bonne liste. Une des choses que vous voudrez peut-être établir - et je ne peux pas vous donner beaucoup de conseils puisque je commence juste à y penser moi-même - est quand une classe doit être dans une bibliothèque, un espace de noms, des espaces de noms imbriqués différents. Vous voudrez peut-être même préparer au préalable une liste de bibliothèques et d'espaces de noms et demander à l'équipe de se réunir et de décider d'en fusionner deux / d'en ajouter un nouveau.
Oh, je viens de penser à quelque chose que je fais et que vous voudrez peut-être aussi. J'ai généralement une bibliothèque de tests unitaires avec un montage de test par politique de classe où chaque test va dans un espace de noms correspondant. J'ai aussi tendance à avoir une autre bibliothèque de tests (tests d'intégration?) Qui est dans un style plus BDD . Cela me permet d'écrire des tests pour spécifier ce que la méthode doit faire ainsi que ce que l'application doit faire en général.
la source
En voici une autre à laquelle j'ai pensé et que j'aime faire.
Si vous prévoyez d'exécuter des tests à partir de l'interface graphique de test unitaire par opposition à TestDriven.Net ou NAnt, j'ai trouvé plus facile de définir le type de projet de test unitaire sur une application console plutôt que sur une bibliothèque. Cela vous permet d'exécuter des tests manuellement et de les parcourir en mode débogage (ce que le TestDriven.Net susmentionné peut réellement faire pour vous).
De plus, j'aime toujours avoir un projet Playground ouvert pour tester des morceaux de code et des idées avec lesquels je ne suis pas familier. Cela ne doit pas être vérifié dans le contrôle de code source. Mieux encore, il devrait être dans un référentiel de contrôle de source séparé sur la machine du développeur uniquement.
la source