Ceci est un extrait de l'eBook Unit Testing Succinctly de Marc Clifton, gracieusement fourni par Syncfusion..
Les articles précédents ont abordé diverses préoccupations et avantages des tests unitaires. Cet article est une analyse plus formalisée des coûts et des avantages des tests unitaires..
Votre code de test unitaire est une entité distincte du code en cours de test, mais il partage un grand nombre des problèmes identiques à ceux requis par votre code de production:
En outre, les tests unitaires peuvent également:
Pour déterminer si les tests peuvent être écrits avec une seule méthode, il convient de prendre en compte:
On doit se rendre compte que le nombre de lignes de code pour tester même une méthode simple pourrait être considérablement plus grand que le nombre de lignes de la méthode elle-même..
Changer le code de production peut souvent invalider les tests unitaires. Les modifications de code tombent grossièrement dans deux catégories:
Le premier supporte généralement peu ou pas de maintenance sur les tests unitaires existants. Toutefois, ces dernières nécessitent souvent de nombreux remaniements unitaires, en fonction de la complexité du changement:
Comme mentionné précédemment, les tests unitaires, en particulier dans un processus piloté par les tests, appliquent certains paradigmes d'architecture et de mise en œuvre minimaux. Pour faciliter davantage la configuration ou la suppression de certaines zones du code, les tests unitaires peuvent également bénéficier de considérations d'architecture plus complexes, telles que l'inversion de contrôle..
Au minimum, la plupart des classes devraient faciliter les moqueries de tout objet. Cela peut considérablement améliorer les performances des tests. Par exemple, tester une méthode qui effectue des contrôles d'intégrité de clé étrangère (plutôt que de compter sur la base de données pour signaler les erreurs ultérieurement) ne devrait pas nécessiter une configuration complexe ou la suppression du scénario de test dans la base de données. lui-même. De plus, la méthode ne devrait pas obliger à interroger la base de données. Ce sont tous des résultats au test qui ajoutent des dépendances à une connexion en direct authentifiée à la base de données et peuvent donc ne pas gérer un autre poste de travail exécutant exactement le même test au même moment. Au lieu de cela, en moquant la connexion à la base de données, le test unitaire peut facilement configurer le scénario en mémoire et transmettre l'objet de connexion en tant qu'interface..
Cependant, le simple fait de se moquer d'une classe n'est pas nécessairement la meilleure pratique non plus. Il serait peut-être préférable de refactoriser le code de manière à obtenir toutes les informations nécessaires à la méthode, en séparant l'acquisition des données du calcul de celles-ci. Maintenant, le calcul peut être effectué sans se moquer de l'objet responsable de l'acquisition des données, ce qui simplifie davantage la configuration du test..
Quelques stratégies d’atténuation des coûts doivent être envisagées..
Le moyen le plus efficace de réduire le coût des tests unitaires consiste à éviter de devoir écrire le test. Bien que cela semble évident, comment cela est-il réalisé? La réponse est de s'assurer que les données transmises à la méthode sont correctes, c'est-à-dire une entrée correcte, une sortie correcte (l'inverse de «garbage in, garbage out»). Oui, vous voulez probablement toujours tester le calcul lui-même, mais si vous pouvez garantir que l'appelant respecte le contrat, il n'est pas particulièrement nécessaire de tester la méthode pour voir si elle gère des paramètres incorrects (violations du contrat)..
Ceci est un peu une pente glissante parce que vous ne savez pas comment la méthode peut être appelée à l'avenir. En fait, vous voudrez peut-être que la méthode valide toujours son contrat, mais dans le cadre dans lequel il est actuellement utilisé, si vous pouvez garantir que le contrat est toujours respecté, il est alors inutile de rédiger des tests par rapport au contrat..
Comment vous assurez-vous que les entrées sont correctes? Une approche consiste à filtrer et à contrôler correctement l'interaction de l'utilisateur afin de pré-filtrer les valeurs provenant d'une interface utilisateur. Une approche plus sophistiquée consiste à définir des types spécialisés plutôt que de s'appuyer sur des types à usage général. Considérons la méthode Divide décrite précédemment:
public static int Divide (num numérateur, dénominateur int) if (dénominateur == 0) jette un nouvel argument ArgumentOutOfRangeException ("Le dénominateur ne peut pas être 0."); retourne numérateur / dénominateur;
Si le dénominateur était un type spécialisé garantissant une valeur non nulle:
Classe publique NonZeroDouble protected int val; public int Value get return val; set if (valeur == 0) lance un nouvel ArgumentOutOfRangeException ("La valeur ne peut pas être 0."); val = valeur;
la méthode Divide n'aurait jamais besoin de tester ce cas:
////// Un exemple d'utilisation de la spécificité de type pour éviter un test de contrat. /// public static int Divide (num numérateur, dénominateur NonZeroDouble) retour numérateur / dénominateur.Valeur;
Quand on considère que cela améliore la spécificité de type de l'application et établit (espérons-le) des types réutilisables, on s'aperçoit que cela évite d'avoir à écrire une série de tests unitaires car le code utilise souvent des types trop généraux..
Demandez-vous si ma méthode est responsable du traitement des exceptions provenant de tiers, tels que services Web, bases de données, connexions réseau, etc. On peut faire valoir que la réponse est «non». Certes, cela nécessite un travail en amont supplémentaire: l'API tierce (ou même l'infrastructure) a besoin d'un wrapper qui gère l'exception et d'une architecture dans laquelle l'état interne de la L'application peut être annulée lorsqu'une exception se produit et devrait probablement être implémentée. Cependant, ces améliorations valent probablement la peine d’être apportées à l’application..
Les exemples précédents - entrées correctes, systèmes de types spécialisés, évitant les exceptions tierces - poussent le problème à un code plus général et éventuellement à du code réutilisable. Cela permet d'éviter d'écrire la même validation ou un contrat similaire, des tests unitaires de gestion des exceptions, et vous permet de vous concentrer sur des tests qui valident ce que la méthode devrait faire dans des conditions normales, à savoir le calcul lui-même..
Comme mentionné précédemment, les tests unitaires présentent des avantages certains en termes de coûts..
L'un des avantages évidents est le processus de formalisation des exigences du code interne à partir des exigences de convivialité / processus externes. Au cours de cet exercice, la direction de l'architecture globale est généralement un avantage secondaire. Plus concrètement, développer une suite de tests permettant de répondre à une exigence spécifique dès le départ. unité perspective (plutôt que la test d'intégration perspective) est la preuve objective que le code met en œuvre l'exigence.
Le test de régression est un autre avantage (souvent mesurable). Au fur et à mesure que la base de code se développe, la vérification que le code existant fonctionne toujours comme prévu réduit considérablement le temps de test manuel et évite les scénarios «oups, nous n'avons pas testé ce critère». De plus, lorsqu'une erreur est signalée, elle peut être corrigée immédiatement, évitant souvent aux autres membres de l'équipe le casse-tête considérable consistant à se demander pourquoi un élément sur lequel ils se fient ne fonctionne tout à coup correctement..
Les tests unitaires vérifient non seulement qu'une méthode se gère correctement lorsque des entrées incorrectes ou des exceptions tierces sont données (comme décrit précédemment, essayez de réduire ce type de tests), mais également comment la méthode devrait se comporter dans des conditions normales. Ceci fournit une documentation précieuse aux développeurs, en particulier aux nouveaux membres de l'équipe. Grâce au test unitaire, ils peuvent facilement comprendre les exigences de configuration et les cas d'utilisation. Si votre projet subit une refactorisation architecturale significative, les nouveaux tests unitaires peuvent être utilisés pour aider les développeurs à retravailler leur code dépendant..
Comme décrit précédemment, une architecture plus robuste grâce à l'utilisation d'interfaces, à l'inversion de contrôle, à des types spécialisés, etc., facilitant tous les tests unitaires.-également améliorer la robustesse de l'application. Les exigences changent, même pendant le développement, et une architecture bien pensée peut gérer ces changements considérablement mieux qu'une application qui n'a pas ou peu de considération architecturale..
Plutôt que de confier à un programmeur junior une exigence de haut niveau à mettre en œuvre au niveau de compétence du programmeur, vous pouvez garantir un niveau de code et de réussite plus élevé (et fournir une expérience d'enseignement) en demandant au programmeur junior de coder la mise en œuvre contre l'épreuve plutôt que l'exigence. Ceci élimine beaucoup de mauvaises pratiques ou de conjectures qu'un programmeur junior finit par mettre en œuvre (nous sommes tous passés par là) et réduit les retouches qu'un développeur plus expérimenté doit faire à l'avenir.
Il existe plusieurs types de révision de code. Les tests unitaires peuvent réduire le temps passé à examiner le code pour résoudre les problèmes d'architecture, car ils ont tendance à appliquer l'architecture. De plus, les tests unitaires valident le calcul et peuvent également être utilisés pour valider tous les chemins de code pour une méthode donnée. Cela rend les révisions de code presque inutiles - le test unitaire devient une auto-révision du code.
Un effet secondaire intéressant de la conversion des exigences d’utilisation ou de processus externes en tests de code formalisés (et leur architecture de support) est le suivant:
À la suite du processus de test unitaire, ces découvertes identifient les problèmes plus tôt dans le processus de développement, ce qui contribue généralement à réduire les risques de confusion, de modification et, partant, de coût.