Tests unitaires stratégies pour les tests unitaires

Les approches de test dépendent de l'endroit où vous vous trouvez dans le projet et de votre «budget» en termes de temps, d'argent, de main-d'œuvre, de besoins, etc. L'idéal est que le test d'unité soit budgétisé dans le processus de développement, mais de manière réaliste, nous rencontrons souvent des programmes existants ou hérités. qui ont peu ou pas de couverture de code mais qui doivent être mis à niveau ou maintenus. 

Le pire scénario est un produit en cours de développement, mais qui présente un nombre croissant d'échecs lors de son développement, là encore avec une couverture de code faible, voire nulle. En tant que chef de produit, soit au début d'un effort de développement, soit à la suite du transfert d'une application existante, il est important de développer une stratégie de test unitaire raisonnable.. 

N'oubliez pas que les tests unitaires doivent fournir des avantages mesurables à votre projet afin de compenser la responsabilité de leur développement, de leur maintenance et de leurs propres tests. De plus, la stratégie que vous adoptez pour vos tests unitaires peut affecter l'architecture de votre application. Bien que ce soit presque toujours une bonne chose, cela peut entraîner des frais généraux inutiles pour vos besoins..

Partir des exigences

Si vous démarrez une application suffisamment complexe à partir d'une table rase, et que tout ce qui est entre vos mains est un ensemble d'exigences, tenez compte des indications suivantes.

Prioriser les exigences informatiques

Donne la priorité aux exigences de calcul de l'application pour déterminer où réside la complexité. La complexité peut être déterminée en découvrant le nombre d'états qu'un calcul particulier doit prendre en compte, ou peut être le résultat d'un grand ensemble de données d'entrée requises pour effectuer le calcul, ou il peut simplement s'agir d'un algorithme complexe, tel que l'analyse des cas d'échec. sur l'anneau de redondance d'un satellite. Indiquez également où le code est susceptible de changer à l'avenir à la suite de modifications inconnues des exigences. Bien que cela semble nécessiter de la clairvoyance, un architecte logiciel expérimenté peut catégoriser le code en un objectif général (résoudre un problème commun) et un domaine spécifique (en résolvant un problème d’exigence spécifique). Ce dernier devient candidat au changement futur.

Tandis que l'écriture de tests unitaires pour des fonctions triviales est facile, rapide et gratifiant du nombre de cas de tests que le programme génère, ce sont les tests les moins coûteux. Ils prennent du temps à écrire et, car ils seront probablement écrits correctement pour commencer et ils ne changeront probablement pas avec le temps, ils sont les moins utiles à mesure que la base de code de l'application se développe. Au lieu de cela, concentrez votre stratégie de tests unitaires sur le code complexe et spécifique à un domaine..

Sélectionnez une architecture

L'un des avantages du démarrage d'un projet à partir d'un ensemble d'exigences est que vous devez créer l'architecture (ou sélectionner une architecture tierce) dans le cadre du processus de développement. Les frameworks tiers qui vous permettent d'exploiter des architectures telles que l'inversion de contrôle (et le concept associé d'injection de dépendance), ainsi que des architectures formelles telles que Model-View-Controller (MVC) et Model-View-ViewModel (MVVM) les tests unitaires pour la simple raison qu’une architecture modulaire est généralement plus facile à tester. Ces architectures séparent:

  • La présentation (voir).
  • Le modèle (responsable de la persistance et de la représentation des données).
  • Le contrôleur (où les calculs devraient être effectués).

Bien que certains aspects du modèle puissent être candidats aux tests unitaires, la plupart des tests unitaires seront probablement écrites par rapport aux méthodes du contrôleur ou du modèle de vue, où les calculs du modèle ou de la vue sont implémentés..

Phase de maintenance

Les tests unitaires peuvent être utiles même si vous êtes impliqué dans la maintenance d'une application, nécessitant soit l'ajout de nouvelles fonctionnalités à une application existante, soit simplement la correction de bogues d'une application existante. Il existe plusieurs approches que l'on peut adopter pour une application existante et les questions sous-jacentes qui peuvent déterminer la rentabilité des tests unitaires:

  • Vous écrivez des tests unitaires uniquement pour les nouvelles fonctionnalités et les corrections de bugs? La fonctionnalité ou la correction de bogue bénéficiera-t-elle des tests de régression ou s'agit-il d'un problème isolé et ponctuel plus facile à tester lors des tests d'intégration??
  • Commencez-vous à écrire des tests unitaires par rapport aux fonctionnalités existantes? Si oui, comment priorisez-vous les fonctionnalités à tester en premier??
  • La base de code existante fonctionne-t-elle bien avec les tests unitaires ou le code doit-il d'abord être refondu pour isoler les unités de code??
  • Quelles configurations ou démontages sont nécessaires pour le test de fonctionnalité ou de bogue?
  • Quelles dépendances peuvent être découvertes au sujet des modifications de code pouvant entraîner des effets secondaires dans un autre code, et si les tests unitaires devaient être élargis pour tester le comportement du code dépendant?

Entrer dans la phase de maintenance d’une application existante dépourvue de tests unitaires n’est pas anodin. La planification, la prise en compte et l’investigation du code peuvent souvent nécessiter plus de ressources que la simple correction du bogue. Cependant, l'utilisation judicieuse des tests unitaires peut être rentable, et bien que cela ne soit pas toujours facile à déterminer, cela en vaut la peine, si cela ne vaut que pour approfondir la compréhension du code de base..


Déterminez votre processus

Trois stratégies peuvent être adoptées en ce qui concerne le processus de test unitaire: le «développement piloté par le test», le «code premier» et, bien que cela puisse sembler contraire au thème de ce livre, le processus du «test sans unité»..

Développement piloté par les tests

L'un des camps est le «développement piloté par les tests», résumé par le flux de travail suivant:

Étant donné une exigence de calcul (voir la section précédente), commencez par écrire un talon pour la méthode..

  • Si des dépendances sur d'autres objets non encore implémentés sont requises (objets transmis en tant que paramètres à la méthode ou renvoyés par la méthode), implémentez-les sous forme d'interfaces vides..
  • Si des propriétés sont manquantes, implémentez des stubs pour les propriétés nécessaires à la vérification des résultats..
  • Écrire toutes les exigences de configuration ou de test de démontage.
  • Ecrivez les tests. Les raisons pour écrire des stubs avant l’écriture du test sont: premièrement, pour tirer parti d’IntelliSense lors de la rédaction du test; deuxièmement, établir que le code est toujours compilé; et troisièmement, pour s'assurer que la méthode testée, ses paramètres, interfaces et propriétés se sont tous synchronisés en ce qui concerne la dénomination..
  • Exécuter les tests en vérifiant qu'ils échouent.
  • Coder l'implémentation.
  • Exécutez les tests en vérifiant qu'ils ont réussi.

En pratique, c'est plus difficile qu'il n'y paraît. Il est facile de tomber dans le piège d'écrire des tests qui ne sont pas rentables, et souvent, on découvre que la méthode testée n'est pas une unité suffisamment fine pour être réellement un bon candidat pour un test. Peut-être que la méthode en fait trop, nécessite trop d’installation ou de démontage, ou a des dépendances sur trop d’objets qui doivent tous être initialisés à un état connu. Ce sont toutes des choses qui sont plus facilement découvertes lors de l'écriture du code, pas le test.

L'un des avantages d'une approche basée sur les tests est que le processus instille d'abord la discipline du test unitaire et de l'écriture des tests unitaires. Il est facile de déterminer si le développeur suit le processus. Avec la pratique, on peut devenir facile à rendre aussi le processus rentable.

Un autre avantage d’une approche axée sur les tests est qu’elle impose, de par sa nature, une sorte d’architecture. Il serait absurde mais faisable d'écrire un test unitaire qui initialise un formulaire, place des valeurs dans un contrôle, puis appelle une méthode censée effectuer un calcul sur les valeurs, comme l'exige ce code (actuellement trouvé ici):

void privé btnCalculate_Click (expéditeur d'objet, System.EventArgs e) double principal, AnnualRate, InterestEarned; double FutureValue, RatePerPeriod; int NumberOfPeriods, CompoundType; Principal = Double.Parse (txtPrincipal.Text); AnnualRate = Double.Parse (txtInterest.Text) / 100; if (rdoMonthly.Checked) CompoundType = 12; else if (rdoQuarterly.Checked) CompoundType = 4; else if (rdoSemiannually.Checked) CompoundType = 2; sinon CompoundType = 1; NumberOfPeriods = Int32.Parse (txtPeriods.Text); double i = AnnualRate / CompoundType; int n = CompoundType * NumberOfPeriods; RatePerPeriod = AnnualRate / NumberOfPeriods; FutureValue = Principal * Math.Pow (1 + i, n); InterestEarned = FutureValue - Principal; txtInterestEarned.Text = InterestEarned.ToString ("C"); txtAmountEarned.Text = FutureValue.ToString ("C"); 

Le code précédent n'est pas testable car il est empêtré dans le gestionnaire d'événements et l'interface utilisateur. On pourrait plutôt écrire la méthode de calcul des intérêts composés:

public enum CompoundType Annuellement = 1, Semestriellement = 2, Trimestriellement = 4, Mensuel = 12 privé double CompoundInterestCalculation (double principal, double annualRate, CompoundType compoundType, int périodes) double annualRateDecimal = annualRate / 100.0; double i = annualRateDecimal / (int) compoundType; int n = (int) compoundType * périodes; double ratePerPeriod = annualRateDecimal / period; double futureValue = principal * Math.Pow (1 + i, n); double interestEaned = futureValue - principal; retour intérêtEaned; 

ce qui permettrait alors d'écrire un test simple:

[TestMethod] public void CompoundInterestTest () intérêt double = CompoundInterestCalculation (2500, 7.55, CompoundType.Monthly, 4); Assert.AreEqual (878,21, intérêt, 0,01); 

De plus, en utilisant des tests paramétrés, il serait simple de tester chaque type de composé, une plage d’années et différents montants d’intérêts et de principal.

L’approche basée sur les tests réellement facilite un processus de développement plus formalisé par la découverte d'unités testables réelles et leur isolement des dépendances transversales.

Code d'abord, test ensuite

Le codage d’abord est plus naturel, ne serait-ce que parce que c’est la manière habituelle de développer des applications. L'exigence et sa mise en œuvre peuvent également sembler assez faciles à première vue, de sorte que l'écriture de plusieurs tests unitaires semble être une mauvaise utilisation du temps. D'autres facteurs tels que les délais peuvent forcer un projet dans un processus de développement «écrivez simplement le code afin que nous puissions expédier».

Le problème avec l'approche du code d'abord est qu'il est facile d'écrire du code qui nécessite le type de test que nous avons vu précédemment. Le code nécessite d’abord une discipline active pour tester le code qui a été écrit. Cette discipline est incroyablement difficile à atteindre, d’autant plus qu’il ya toujours la nouvelle fonctionnalité à implémenter..

Si vous le souhaitez, vous devez également faire preuve d'intelligence pour éviter d'écrire un code de franchissement de frontière intriqué et la discipline nécessaire. Qui n'a pas cliqué sur un bouton du concepteur Visual Studio et codé le calcul de l'événement dans le stub créé par Visual Studio pour vous? C'est simple et parce que l'outil vous oriente dans cette direction, le programmeur naïf va penser que c'est la bonne façon de coder.

Cette approche nécessite un examen minutieux des compétences et de la discipline de votre équipe, et nécessite un suivi plus étroit de l'équipe, en particulier pendant les périodes de stress élevé, lorsque des approches disciplinées ont tendance à s'effondrer. Certes, une discipline axée sur les tests peut également être rejetée à l'approche des délais, mais il s'agit généralement d'une décision consciente de faire une exception, alors que cela peut facilement devenir la règle dans une approche de code d'abord..

Pas de tests unitaires

Ce n’est pas parce que vous n’avez pas de tests unitaires que vous vous disputez. Il se peut simplement que les tests mettent l'accent sur les procédures de tests d'acceptation ou de tests d'intégration.

Équilibrer les stratégies de test

Un processus de test unitaire rentable nécessite un équilibre entre les stratégies de développement piloté par le test, de code d'abord, de test en second et de test d'une autre manière. La rentabilité des tests unitaires doit toujours être prise en compte, ainsi que des facteurs tels que l'expérience des développeurs de l'équipe. En tant que manager, vous ne voudrez peut-être pas entendre dire qu'une approche basée sur les tests est une bonne idée si votre équipe est assez verte et que vous avez besoin du processus pour inculquer la discipline et l'approche..