Meilleures pratiques de développement piloté par les tests utilisant C # et RhinoMocks [fermé]

86

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:

  1. É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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. Isolez les dépendances externes. Raison: les dépendances externes non résolues ne peuvent pas être testées.

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

Kevin Albrecht
la source
Ceci est une liste utile. Nous utilisons actuellement NUnit et Rhino.Mocks, et il est bon de préciser ces critères pour les membres de l'équipe qui sont moins familiers avec cet aspect des tests unitaires.
Chris Ballard

Réponses:

58

Certainement une bonne liste. Voici quelques réflexions à ce sujet:

Écrivez d'abord le test, puis le code.

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.

Concevez des classes à l'aide de l'injection de dépendances.

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.

Séparez le code d'interface utilisateur de son comportement à l'aide de Model-View-Controller ou Model-View-Presenter.

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.

N'écrivez pas de méthodes ou de classes statiques.

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.

Programmez des interfaces, pas des classes.

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.

Isolez les dépendances externes.

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).

Marquez comme virtuelles les méthodes que vous souhaitez vous moquer.

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é.)

aridlehoover
la source
3
Je trouve généralement que si un test est difficile à lire, ce n'est pas la faute du framework mais du code qu'il teste. Si le SUT est compliqué à mettre en place, il devrait peut-être être divisé en plusieurs concepts.
Steve Freeman
10

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));
Zadam
la source
5
Je pense que la nouvelle version de Rhino Mocks fonctionne aussi comme ça
George Mauer
3

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
2

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.

Steve Freeman
la source
D'accord ... Je vais mettre à jour ma question pour refléter cela.
Kevin Albrecht le
1

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.

George Mauer
la source
Je fais également une section de test de style BDD similaire (en plus du code de test unitaire) dans un projet personnel.
Kevin Albrecht
0

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.

George Mauer
la source