Doit-on tester la mise en œuvre interne ou tester uniquement le comportement du public?

Logiciel donné où …

  • Le système se compose de quelques sous-systèmes
  • Chaque sous-système se compose de quelques composants
  • Chaque composant est implémenté en utilisant plusieurs classes

… J’aime écrire des tests automatisés pour chaque sous-système ou composant.

Je n’écris pas de test pour chaque classe interne d’un composant (sauf dans la mesure où chaque classe consortingbue à la fonctionnalité publique du composant et est donc testable / testée de l’extérieur via l’API publique du composant).

Lorsque je refactore l’implémentation d’un composant (ce que je fais souvent, dans le cadre de l’ajout de nouvelles fonctionnalités), je n’ai donc pas besoin de modifier les tests automatisés existants, car les tests dépendent uniquement de l’API publique du composant et des API publiques. sont généralement développés plutôt que modifiés.

Je pense que cette politique contraste avec un document comme Refactoring Test Code , qui dit des choses comme …

  • “… tests unitaires …”
  • “… une classe de test pour chaque classe du système …”
  • “… le code de test / rapport de code de production … est idéalement considéré comme proche d’un ratio de 1: 1 …”

… Je suppose que je ne suis pas d’accord (ou du moins ne pratique pas).

Ma question est la suivante: si vous n’êtes pas d’accord avec ma politique, pourriez-vous expliquer pourquoi? Dans quels scénarios ce degré de test est-il insuffisant?

En résumé:

  • Les interfaces publiques sont testées (et testées à nouveau) et changent rarement (elles sont ajoutées mais rarement modifiées)
  • Les API internes sont cachées derrière les API publiques et peuvent être modifiées sans réécrire les scénarios de test qui testent les API publiques

Note de bas de page: certains de mes «cas de test» sont effectivement mis en œuvre sous forme de données. Par exemple, les scénarios de test pour l’interface utilisateur se composent de fichiers de données contenant diverses entrées utilisateur et les sorties système correspondantes. Tester le système signifie avoir un code de test qui lit chaque fichier de données, relit l’entrée dans le système et affirme qu’il obtient la sortie attendue correspondante.

Bien que je n’aie que rarement besoin de changer le code de test (car les API publiques sont généralement ajoutées plutôt que modifiées), je trouve que parfois (par exemple deux fois par semaine) j’ai besoin de modifier certains fichiers de données existants. Cela peut se produire lorsque je modifie la sortie du système (une nouvelle fonctionnalité améliore la sortie existante), ce qui peut entraîner l’échec d’un test existant (car le code de test tente uniquement d’affirmer que la sortie n’a pas changé). Pour gérer ces cas, je fais ce qui suit:

  • Réexécutez la suite de tests automatisée qui dispose d’un indicateur d’exécution spécial, qui lui indique de ne pas activer la sortie, mais plutôt de capturer la nouvelle sortie dans un nouveau répertoire.
  • Utilisez un outil de comparaison visuelle pour voir quels fichiers de données de sortie (c.-à-d. Quels scénarios de test) ont changé, et pour vérifier que ces modifications sont correctes et conformes aux nouvelles fonctionnalités.
  • Mettez à jour les tests existants en copiant les nouveaux fichiers de sortie du nouveau répertoire dans le répertoire à partir duquel les scénarios de test sont exécutés (écrasez les anciens tests)

Note de bas de page: par “composant”, je veux dire quelque chose comme “une DLL” ou “un assemblage” … quelque chose d’assez grand pour être visible sur une architecture ou un diagramme de déploiement du système, souvent implémenté avec des dizaines ou 100 classes. avec une API publique composée d’environ 1 ou d’une poignée d’interfaces … quelque chose pouvant être assigné à une équipe de développeurs (où un composant différent est assigné à une équipe différente), et qui selon la loi de Conway une API publique relativement stable.


Footnote: L’article Test orienté object: mythe et réalité dit:

Mythe: Le test des boîtes noires est suffisant. Si vous effectuez un travail minutieux de conception de cas de test en utilisant l’interface ou la spécification de classe, vous pouvez être assuré que la classe a été pleinement utilisée. Les tests en boîte blanche (en regardant l’implémentation d’une méthode pour concevoir des tests) violent le concept même d’encapsulation.

Réalité: OO structure, partie II. De nombreuses études ont montré que les suites de tests de boîte noire considérées comme extrêmement sévères par les développeurs n’exercent qu’un tiers à la moitié des déclarations (sans parler des chemins ou des états) dans l’implémentation testée. Il y a trois raisons à cela. Premièrement, les entrées ou états sélectionnés exercent généralement des chemins normaux, mais ne forcent pas tous les chemins / états possibles. Deuxièmement, les tests de boîte noire ne peuvent à eux seuls révéler des sursockets. Supposons que nous avons testé tous les comportements spécifiés du système testé. Pour être sûr qu’il n’existe pas de comportements non spécifiés, nous devons savoir si des parties du système n’ont pas été utilisées par la suite de tests Black Box. La seule façon d’obtenir ces informations est d’utiliser une instrumentation par code. Troisièmement, il est souvent difficile d’exécuter des exceptions et de traiter des erreurs sans examiner le code source.

Je devrais append que je fais des tests fonctionnels de whitebox: je vois le code (dans l’implémentation) et j’écris des tests fonctionnels (qui pilotent l’API publique) pour exercer les différentes twigs du code (détails de l’implémentation de la fonctionnalité).

Ma pratique consiste à tester les composants internes via l’interface API / UI publique. Si un code interne ne peut pas être atteint de l’extérieur, alors je refactore pour le supprimer.

La réponse est très simple: vous décrivez les tests fonctionnels, qui constituent une partie importante de l’assurance qualité du logiciel. Tester la mise en œuvre interne est un test unitaire, qui constitue une autre partie de l’assurance qualité du logiciel avec un objective différent. C’est pourquoi vous sentez que les gens ne sont pas d’accord avec votre approche.

Les tests fonctionnels sont importants pour vérifier que le système ou le sous-système fait ce qu’il est censé faire. Tout ce que le client voit doit être testé de cette façon.

Unit-test est là pour vérifier que les 10 lignes de code que vous venez d’écrire font ce qu’il est censé faire. Cela vous donne plus de confiance sur votre code.

Les deux sont complémentaires. Si vous travaillez sur un système existant, le test fonctionnel est probablement la première chose à faire. Mais dès que vous ajoutez du code, le test unitaire est également une bonne idée.

Je n’ai pas mon exemplaire de Lakos devant moi, alors plutôt que de le citer, je ferai simplement remarquer qu’il fait un meilleur travail que moi pour expliquer pourquoi le test est important à tous les niveaux.

Le problème avec le seul test de “comportement public” est qu’un tel test vous donne très peu d’informations. Il va attraper beaucoup de bogues (tout comme le compilateur va attraper beaucoup de bogues), mais ne peut pas vous dire où sont les bogues. Il est courant pour une unité mal implémentée de renvoyer de bonnes valeurs pendant une longue période, puis d’arrêter de le faire lorsque les conditions changent; Si cette unité avait été testée directement, le fait qu’elle ait été mal mise en œuvre aurait été évident plus tôt.

Le meilleur niveau de granularité de test est le niveau de l’unité. Fournir des tests pour chaque unité via ses interfaces. Cela vous permet de valider et de documenter vos croyances sur le comportement de chaque composant, ce qui vous permet de tester le code dépendant en ne testant que la nouvelle fonctionnalité introduite, ce qui permet de garder les tests courts et ciblés. En bonus, il garde les tests avec le code qu’ils testent.

Pour le formuler différemment, il est correct de ne tester que le comportement public, du moment que vous remarquez que chaque classe publiquement visible a un comportement public.

Il y a eu beaucoup de bonnes réponses à cette question jusqu’à présent, mais je veux append quelques notes de mon côté. En guise de préface: je suis consultant pour une grande entreprise qui fournit des solutions technologiques à un large éventail de grands clients. Je dis cela parce que, selon mon expérience, nous sums obligés de tester beaucoup plus que la plupart des logiciels (sauf peut-être les développeurs d’API). Voici quelques étapes à suivre pour garantir la qualité:

  • Test d’unité interne:
    Les développeurs doivent créer des tests unitaires pour tout le code qu’ils écrivent (lisez: chaque méthode). Les tests unitaires doivent couvrir les conditions de test positives (ma méthode fonctionne-t-elle?) Et les conditions de test négatives (la méthode lance-t-elle une ArgumentNullException lorsque l’un de mes arguments requirejs est null?). Nous incorporons généralement ces tests dans le processus de construction en utilisant un outil comme CruiseControl.net
  • Test du système / test d’assemblage:
    Parfois, cette étape est appelée quelque chose de différent, mais c’est lorsque nous commençons à tester la fonctionnalité publique. Une fois que vous savez que toutes vos unités fonctionnent comme prévu, vous voulez savoir que vos fonctions externes fonctionnent également comme vous le souhaitez. Il s’agit d’une forme de vérification fonctionnelle car l’objective est de déterminer si l’ensemble du système fonctionne comme il se doit. Notez que cela n’inclut aucun point d’intégration. Pour le test du système, vous devez utiliser des interfaces simulées plutôt que les vraies afin de pouvoir contrôler la sortie et créer des scénarios de test autour.
  • Test d’intégration système:
    À ce stade du processus, vous souhaitez connecter vos points d’intégration au système. Par exemple, si vous utilisez un système de traitement de carte de crédit, vous devrez incorporer le système en direct à ce stade pour vérifier qu’il fonctionne toujours. Vous souhaitez effectuer des tests similaires au test système / assemblage.
  • Test de vérification fonctionnelle:
    La vérification fonctionnelle est effectuée par les utilisateurs exécutant le système ou utilisant l’API pour vérifier qu’elle fonctionne comme prévu. Si vous avez construit un système de facturation, c’est à ce stade que vous exécuterez vos scripts de test de bout en bout pour vous assurer que tout fonctionne comme vous l’avez conçu. C’est évidemment une étape critique du processus, car cela vous indique si vous avez fait votre travail.
  • Test de certificateion:
    Ici, vous mettez de vrais utilisateurs devant le système et laissez-les tenter. Idéalement, vous avez déjà testé votre interface utilisateur avec vos parties prenantes, mais cette étape vous dira si votre public cible aime votre produit. Vous avez peut-être entendu dire quelque chose comme un “candidat à la libération” par d’autres fournisseurs. Si tout se passe bien à ce stade, vous savez que vous êtes prêt à passer à la production. Les tests de certificateion doivent toujours être effectués dans le même environnement que vous utiliserez pour la production (ou au moins dans un environnement identique).

Bien sûr, je sais que tout le monde ne suit pas ce processus, mais si vous le regardez de bout en bout, vous pouvez commencer à voir les avantages de chaque composant. Je n’ai pas inclus de choses comme les tests de vérification de la construction, car ils se produisent dans un calendrier différent (par exemple, quotidiennement). Personnellement, je pense que les tests unitaires sont essentiels, car ils vous permettent de savoir avec précision quel composant spécifique de votre application est défaillant et dans quel cas d’utilisation spécifique. Les tests unitaires vous aideront également à identifier les méthodes qui fonctionnent correctement afin que vous ne passiez pas votre temps à les rechercher pour plus d’informations sur un échec quand elles ne sont pas défectueuses.

Bien sûr, les tests unitaires peuvent également être erronés, mais si vous développez vos scénarios de test à partir de vos spécifications fonctionnelles / techniques (vous en avez une, non?)), Vous ne devriez pas avoir trop de problèmes.

Si vous pratiquez un développement purement axé sur les tests, vous implémentez uniquement du code après avoir effectué un test défaillant et implémentez uniquement du code de test lorsque vous n’avez aucun test échoué. De plus, n’implémentez que la chose la plus simple pour faire un test d’échec ou de réussite.

Dans la pratique limitée de TDD, j’ai vu comment cela m’aidait à éliminer les tests unitaires pour chaque condition logique produite par le code. Je ne suis pas tout à fait convaincu que 100% des fonctionnalités logiques de mon code privé sont exposées par mes interfaces publiques. Pratiquer TDD semble être complémentaire à cette mésortingque, mais il existe encore des fonctionnalités cachées non autorisées par les API publiques.

Je suppose que vous pourriez dire que cette pratique me protège contre les défauts futurs de mes interfaces publiques. Soit vous trouvez cela utile (et vous permet d’append de nouvelles fonctionnalités plus rapidement) ou vous trouvez que c’est une perte de temps.

Vous pouvez coder des tests fonctionnels; C’est très bien. Mais vous devez valider l’utilisation de la couverture de test sur l’implémentation, pour démontrer que le code testé a tous une utilité par rapport aux tests fonctionnels et qu’il fait réellement quelque chose de pertinent.

Vous ne devriez pas penser aveuglément qu’une unité == une classe. Je pense que cela peut être contre-productif. Quand je dis que j’écris un test unitaire, je teste une unité logique – “quelque chose” qui fournit un comportement. Une unité peut être une classe unique ou plusieurs classes peuvent travailler ensemble pour fournir ce comportement. Parfois, il s’agit d’une classe unique, mais évolue pour devenir trois ou quatre classes plus tard.

Si je commence avec une classe et que j’écris des tests pour cela, mais plus tard cela devient plusieurs classes, je n’écrirai généralement pas de tests séparés pour les autres classes – ce sont des détails d’implémentation dans l’unité testée. De cette façon, je permet à mon design de grandir et mes tests ne sont pas si fragiles.

J’avais l’habitude de penser exactement comme CrisW Démonstartes dans cette question – que tester à des niveaux plus élevés serait mieux, mais après avoir eu plus d’expérience, mes pensées sont modérées à quelque chose entre cela et “chaque classe devrait avoir une classe de test”. Chaque unité devrait avoir des tests, mais je choisis de définir mes unités légèrement différentes de ce que j’ai déjà fait. Ce pourrait être les “composants” dont parle CrisW, mais très souvent, il ne s’agit que d’une seule classe.

De plus, les tests fonctionnels peuvent suffire à prouver que votre système fait ce qu’il est censé faire, mais si vous souhaitez piloter votre conception avec des exemples / tests (TDD / BDD), des tests à levier inférieur sont une conséquence naturelle. Vous pouvez jeter ces tests de bas niveau lorsque vous avez terminé la mise en œuvre, mais ce serait un gaspillage – les tests sont un effet secondaire positif. Si vous décidez de procéder à des refactorisations radicales invalidant vos tests de bas niveau, vous les jetez et vous écrivez une nouvelle fois.

Séparer l’objective de tester / prouver votre logiciel et utiliser des tests / exemples pour piloter votre conception / implémentation peut clarifier cette discussion.

Mise à jour: En outre, il y a essentiellement deux manières de faire TDD: à l’extérieur et à l’intérieur. Le BDD favorise l’extérieur, ce qui conduit à des tests / spécifications de plus haut niveau. Si vous partez des détails, vous écrirez des tests détaillés pour toutes les classes.

Je suis d’accord avec la plupart des articles ici, mais j’appendais ceci:

Il est primordial de tester les interfaces publiques, puis protégées, puis privées.

Généralement, les interfaces publiques et protégées résument une combinaison d’interfaces privées et protégées.

Personnellement: vous devriez tout tester. Étant donné un ensemble de tests solides pour des fonctions plus petites, vous aurez une plus grande confiance dans le fait que les méthodes cachées fonctionnent. Aussi, je suis d’accord avec le commentaire d’une autre personne sur le refactoring. La couverture du code vous aidera à déterminer où sont les bits de code supplémentaires et à les restructurer si nécessaire.

Suivez-vous toujours cette approche? Je crois aussi que c’est la bonne approche. Vous devez uniquement tester les interfaces publiques. Maintenant, l’interface publique peut être un service ou un composant qui prend en charge une interface utilisateur ou toute autre source.

Mais vous devriez pouvoir faire évoluer le service ou le composant puplic en utilisant l’approche Test First. C’est-à-dire définir une interface publique et la tester pour les fonctionnalités de base. ça va échouer. Implémentez cette fonctionnalité de base en utilisant l’API de classes d’arrière-plan. Ecrire API pour satisfaire uniquement ce premier test. Puis continuez à demander ce que le service peut faire plus et évoluer.

Seule une décision équilibrée consiste à diviser le gros service ou composant en quelques services et composants plus petits pouvant être réutilisés. Si vous croyez fermement qu’un composant peut être réutilisé dans les projets. Ensuite, des tests automatisés doivent être écrits pour ce composant. Mais là encore, les tests écrits pour le gros service ou le composant doivent dupliquer le composant déjà testé en tant que composant.

Certaines personnes peuvent entrer dans une discussion théorique sur le fait qu’il ne s’agit pas de tests unitaires. Donc ça va. L’idée de base est d’avoir des tests automatisés qui testent votre logiciel. Alors si ce n’est pas au niveau de l’unité. Si elle couvre l’intégration avec la firebase database (que vous contrôlez), alors c’est seulement mieux.

Faites-moi savoir si vous avez développé un bon processus qui fonctionne pour vous … depuis votre premier post ..

Sincères salutations

Je teste personnellement les parties protégées car elles sont “publiques” aux types hérités …

Je conviens que la couverture du code devrait idéalement être de 100%. Cela ne signifie pas nécessairement que 60 lignes de code auront 60 lignes de code de test, mais que chaque chemin d’exécution est testé. La seule chose de plus ennuyeuse qu’un bogue est un bogue qui n’a pas encore été exécuté.

En ne testant que l’API publique, vous courez le risque de ne pas tester toutes les instances des classes internes. Je dis vraiment l’évidence en disant cela, mais je pense que cela devrait être mentionné. Plus chaque comportement est testé, plus il est facile de reconnaître non seulement qu’il est cassé, mais aussi ce qui est cassé.

Je teste les détails de l’implémentation privée ainsi que les interfaces publiques. Si je modifie un détail d’implémentation et que la nouvelle version présente un bogue, cela me permet de mieux comprendre où se trouve l’erreur et pas seulement ce qu’elle produit.

[Une réponse à ma propre question]

L’une des variables les plus importantes est peut-être le nombre de programmeurs différents:

  • Axiom: chaque programmeur doit tester son propre code

  • Par conséquent: si un programmeur écrit et délivre une “unité”, il devrait également avoir testé cette unité, probablement en écrivant un “test unitaire”.

  • Corollaire: si un seul programmeur écrit un paquet entier, alors il suffit au programmeur d’écrire des tests fonctionnels sur l’ensemble du paquet (inutile d’écrire des tests «unitaires» des unités dans le paquet, puisque ces unités sont des détails d’implémentation n’a pas d’access direct / exposition).

De même, la pratique de la construction de composants “simulés” sur lesquels vous pouvez tester:

  • Si vous avez deux équipes qui construisent deux composants, chacun peut avoir besoin de “se moquer” du composant de l’autre pour avoir quelque chose (la maquette) sur lequel tester son propre composant, avant que son composant soit jugé prêt pour les “tests d’intégration” ultérieurs. avant que l’autre équipe ait livré son composant sur lequel votre composant peut être testé.

  • Si vous développez l’ensemble du système, vous pouvez développer l’ensemble du système … par exemple, développer un nouveau champ GUI, un nouveau champ de firebase database, une nouvelle transaction commerciale et un nouveau système / test fonctionnel, le tout dans le cadre d’un seul itération, sans avoir besoin de développer des “simulacres” de calques (puisque vous pouvez plutôt tester avec le vrai).

Axiom: chaque programmeur doit tester son propre code

Je ne pense pas que ce soit universellement vrai.

En cryptographie, il y a un dicton bien connu: “il est facile de créer un chiffrement si sécurisé que vous ne savez pas comment le casser vous-même”.

Dans votre processus de développement classique, vous écrivez votre code, puis vous le comstackz et l’exécutez pour vérifier qu’il fait ce que vous pensez. Répétez cette opération beaucoup de temps et vous vous sentirez plutôt en confiance avec votre code.

Votre confiance fera de vous un testeur moins vigilant. Celui qui ne partage pas votre expérience avec le code n’aura pas le problème.

De plus, une nouvelle paire d’œil peut avoir moins d’idées préconçues non seulement sur la fiabilité du code, mais aussi sur ce que fait le code. En conséquence, ils peuvent proposer des cas de test auxquels l’auteur du code n’a pas pensé. On pourrait s’attendre à ce que ceux-ci découvrent un plus grand nombre de bogues ou diffusent un peu plus d’informations sur ce que le code fait dans l’organisation.

De plus, il faut argumenter que pour être un bon programmeur, vous devez vous soucier des cas extrêmes, mais pour être un bon testeur, vous devez vous inquiéter de façon obsessionnelle 😉 les testeurs peuvent également être moins chers. équipe de test pour cette raison.

Je pense que la question primordiale est la suivante: quelle est la meilleure méthode pour trouver des bogues dans les logiciels? J’ai récemment regardé une vidéo (sans lien, désolé) indiquant que les tests randomisés sont moins chers et aussi efficaces que les tests générés par l’homme.

Cela dépend de votre conception et de la plus grande valeur. Un type d’application peut exiger une approche différente de l’autre. Parfois, vous n’acceptez guère d’intérêt avec les tests unitaires, alors que les tests fonctionnels / d’intégration génèrent des sursockets. Parfois, les tests unitaires échouent des centaines de fois au cours du développement, attrapant de nombreux bogues en cours de réalisation.

Parfois, c’est sortingvial. Le rapprochement de certaines classes rend le retour sur investissement des tests de chaque chemin moins attrayant, vous pouvez donc simplement tracer une ligne et passer à la mise au sharepoint quelque chose de plus important / compliqué / lourdement utilisé.

Parfois, il ne suffit pas de tester l’API publique car une logique particulièrement intéressante se cache à l’intérieur, et il est trop pénible de mettre le système en mouvement et d’exercer ces chemins particuliers. C’est à ce moment que tester les entrailles est payant.

Ces jours-ci, j’ai tendance à écrire de nombreuses classes (souvent extrêmement) simples qui font une ou deux choses en plus. J’implémente ensuite le comportement souhaité en déléguant toutes les fonctionnalités compliquées à ces classes internes. Ie j’ai des interactions légèrement plus complexes, mais des classes très simples.

Si je modifie mon implémentation et que je dois refactoriser certaines de ces classes, je m’en fiche généralement. Je garde mes tests isolés du mieux que je peux, donc c’est souvent un changement simple pour les faire fonctionner à nouveau. Cependant, si je dois éliminer certaines classes internes, je remplace souvent une poignée de classes et écris des tests entièrement nouveaux. J’entends souvent des gens se plaindre d’avoir à tenir les tests à jour après le refactoring et, bien que cela soit parfois inévitable et fastidieux, si le niveau de granularité est suffisant, il n’est généralement pas grave de jeter des tests code +.

Je pense que c’est l’une des différences majeures entre la conception pour la testabilité et le fait de ne pas déranger.