Ceci est un extrait de l'eBook Unit Testing Succinctly de Marc Clifton, gracieusement fourni par Syncfusion..
Le test unitaire consiste à prouver l'exactitude. Pour prouver que quelque chose fonctionne correctement, vous devez d’abord comprendre ce qu’un unité et un tester sont en fait avant que vous puissiez explorer ce qui est prouvable dans les capacités de tests unitaires.
Dans le contexte des tests unitaires, une unité possède plusieurs caractéristiques.
Une unité pure est la méthode la plus simple et la plus idéale pour écrire un test unitaire. Une unité pure a plusieurs caractéristiques qui facilitent les tests.
Une unité devrait (idéalement) ne pas appeler d'autres méthodes
En ce qui concerne les tests unitaires, une unité doit avant tout être une méthode qui fait quelque chose sans faire appel à aucune autre méthode. Des exemples de ces unités pures se trouvent dans le Chaîne
et Math
les classes - la plupart des opérations effectuées ne reposent sur aucune autre méthode. Par exemple, le code suivant (tiré de quelque chose que l'auteur a écrit)
Void public SelectedMasters () string currentEntity = dgvModel.DataMember; string navToEntity = cbMasterTables.SelectedItem.ToString (); DataGridViewSelectedRowCollection selectedRows = dgvModel.SelectedRows; StringBuilder qualifier = BuildQualifier (selectedRows); UpdateGrid (navToEntity); SetRowFilter (navToEntity, qualifier.ToString ()); ShowNavigateToMaster (navToEntity, qualifier.ToString ());
ne doit pas être considéré comme une unité pour trois raisons:
La première raison met en évidence un problème subtil: les propriétés doivent être considérées comme des appels de méthode. En fait, ils sont dans l'implémentation sous-jacente. Si votre méthode utilise les propriétés d’autres classes, il s’agit d’un type d’appel de méthode qui doit être considéré avec soin lors de l’écriture d’une unité appropriée..
En réalité, ce n'est pas toujours possible. Assez souvent, un appel au framework ou à une autre API est requis pour que l'unité fonctionne correctement. Cependant, ces appels doivent être inspectés pour déterminer si la méthode peut être améliorée pour créer une unité plus pure, par exemple, en extrayant les appels dans une méthode supérieure et en transmettant les résultats des appels en tant que paramètre à l'unité..
Un corollaire à «une unité ne devrait pas appeler d’autres méthodes» est qu’une unité est une méthode qui: fait une chose et une seule chose. Souvent, d'autres méthodes sont appelées pour faire plus d'une chose-une habileté précieuse pour savoir quand une tâche est composée de plusieurs sous-tâches, même si elle peut être décrite comme une tâche de haut niveau, ce qui la fait sonner comme une tâche unique!
Le code suivant peut ressembler à une unité raisonnable qui fait une chose: il insère un nom dans la base de données..
public int Insert (personne personne) DbProviderFactory factory = SqlClientFactory.Instance; using (connexion DbConnection = factory.CreateConnection ()) connection.ConnectionString = "Serveur = localhost; Base de données = myDataBase; Trusted_Connection = True;"; connection.Open (); using (DbCommand command = connection.CreateCommand ()) command.CommandText = "insérer dans PERSON (ID, NOM) des valeurs (@Id, @Name)"; command.CommandType = CommandType.Text; DbParameter id = command.CreateParameter (); id.ParameterName = "@Id"; id.DbType = DbType.Int32; id.Value = person.Id; DbParameter name = command.CreateParameter (); name.ParameterName = "@Name"; name.DbType = DbType.String; name.Size = 50; name.Value = person.Name; command.Parameters.AddRange (new DbParameter [] id, name); int rowsAffected = command.ExecuteNonQuery (); renvoyer les lignes affectées;
Cependant, ce code fait plusieurs choses:
SqlClient
instance de fournisseur d'usine.Ce code présente une variété de problèmes qui le rendent inadéquat et le rend difficile à réduire en unités de base. Une meilleure façon d'écrire ce code pourrait ressembler à ceci:
public int RefactoredInsert (Personne personne) Usine DbProviderFactory = SqlClientFactory.Instance; using (conn = connectConnection = OpenConnection (fabrique, "serveur = localhost; base = myDataBase; Trusted_Connection = True;")) using (DbCommand cmd = CreateTextCommand (conn, ", insérez dans PERSON (ID, NOM) valeurs (@Id, @) Name) ")) AddParameter (cmd," @Id ", person.Id); AddParameter (cmd, "@Name", 50, person.Name); int rowsAffected = cmd.ExecuteNonQuery (); renvoyer les lignes affectées; protected DbConnection OpenConnection (fabrique DbProviderFactory, chaîne connectString) DbConnection conn = factory.CreateConnection (); conn.ConnectionString = connectString; conn.Open (); retour conn; protected DbCommand CreateTextCommand (connexion DbConnection, chaîne de caractères cmdText) DbCommand cmd = conn.CreateCommand (); cmd.CommandText = cmdText; cmd.CommandType = CommandType.Text; return cmd; void protected AddParameter (DbCommand cmd, string paramName, int paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.Int32; param.Value = paramValue; cmd.Parameters.Add (param); void protected AddParameter (DbCommand cmd, string paramName, int size, string paramValue) DbParameter param = cmd.CreateParameter (); param.ParameterName = paramName; param.DbType = DbType.String; param.Size = taille; param.Value = paramValue; cmd.Parameters.Add (param);
Remarquez comment, en plus de paraître plus propre, les méthodes OpenConnection
, CreateTextCommand
, et AddParameter
sont plus adaptés aux tests unitaires (en ignorant le fait qu’il s’agit de méthodes protégées). Ces méthodes ne font qu’une chose et, en tant qu’unités, peuvent être testées pour s’assurer qu’elles le font correctement. De là, il devient peu utile de tester la RefactoredInsert
méthode, car il repose entièrement sur d'autres fonctions qui ont des tests unitaires. Au mieux, on peut vouloir écrire quelques cas de test de gestion des exceptions, et éventuellement une validation sur les champs de La personne
table.
Et si la méthode de niveau supérieur faisait plus que simplement appeler d’autres méthodes pour lesquelles il existe des tests unitaires, par exemple, une sorte de calcul supplémentaire? Dans ce cas, le code effectuant le calcul doit être déplacé vers sa propre méthode, des tests doivent être écrits pour ce calcul et, là encore, la méthode de niveau supérieur peut s'appuyer sur l'exactitude du code qu'elle appelle. C’est le processus de construction d’un code parfaitement correct. L'exactitude des méthodes de niveau supérieur s'améliore lorsqu'elles se contentent d'appeler des méthodes de niveau inférieur qui ont des preuves (tests unitaires) d'exactitude..
La complexité cyclomatique constitue le fléau des tests unitaires et des tests d’application en général, car elle augmente la difficulté de tester tous les chemins de code. Idéalement, une unité n'aura aucun si
ou commutateur
déclarations. Le corps de ces déclarations doit être considéré comme les unités (en supposant qu'elles remplissent les autres critères d'une unité) et, pour pouvoir être testé, doit être extrait selon leurs propres méthodes..
Voici un autre exemple tiré du projet MyXaml de l'auteur (partie de l'analyseur):
if (tagName == "*") foreach (noeud XmlNode dans topElement.ChildNodes) if (! (noeud est XmlComment)) objectNode = noeud; Pause; foreach (XmlAttribute attr dans objectNode.Attributes) if (attr.LocalName == "Nom") nameAttr = attr; Pause; else … etc…
Nous avons ici plusieurs chemins de code impliquant si
, autre
, et pour chaque
déclarations qui:
Évidemment, les branchements conditionnels, les boucles, les instructions de cas, etc. ne peuvent pas être évités, mais il peut être intéressant d’envisager de refactoriser le code afin que les éléments internes des conditions et des boucles soient des méthodes distinctes pouvant être testés indépendamment. Ensuite, les tests de la méthode de niveau supérieur peuvent simplement garantir que les états (représentés par des conditions, des boucles, des commutateurs, etc.) sont gérés correctement, indépendamment des calculs effectués..
Les méthodes ayant des dépendances sur d'autres classes, données et informations d'état sont plus complexes à tester, car elles dépendent des exigences relatives aux objets instanciés, à l'existence de données et à un état prédéterminé..
Dans sa forme la plus simple, les unités dépendantes ont des conditions préalables qui doivent être remplies. Les moteurs de tests unitaires fournissent des mécanismes permettant d'instancier des dépendances de test, à la fois pour des tests individuels et pour tous les tests d'un groupe de tests, ou «fixture».
Les unités dépendantes complexes nécessitent des services tels que les connexions de base de données pour être instanciées ou simulées. Dans l'exemple de code précédent, le Insérer
Cette méthode ne peut pas être testée par unité sans la possibilité de se connecter à une base de données réelle. Ce code devient plus testable si l’interaction de la base de données peut être simulée, généralement à l’aide d’interfaces ou de classes de base (abstraites ou non)..
Les méthodes refactorisées dans le Insérer
code décrit plus tôt sont un bon exemple parce que DbProviderFactory
est une classe de base abstraite, donc on peut facilement créer une classe dérivant de DbProviderFactory
simuler la connexion à la base de données.
Les unités dépendantes, parce qu'elles appellent d'autres méthodes ou API, sont également plus fragiles: elles peuvent avoir besoin de gérer explicitement les erreurs potentiellement générées par les méthodes qu'elles appellent. Dans l'exemple de code précédent, le Insérer
Le code de la méthode pourrait être encapsulé dans un bloc try-catch, car il est certainement possible que la connexion à la base de données n'existe pas. Le gestionnaire d'exceptions peut renvoyer 0
pour le nombre de lignes affectées, en signalant l'erreur par un autre mécanisme. Dans un tel scénario, les tests unitaires doivent être capables de simuler cette exception pour s'assurer que tous les chemins de code sont exécutés correctement, notamment: capture
et enfin
des blocs.
Un test fournit une affirmation utile de l'exactitude de l'unité. Les tests qui affirment l'exactitude d'une unité exercent généralement l'unité de deux manières:
Tester le comportement de l'unité dans des conditions normales est de loin le test le plus facile à écrire. Après tout, lorsque nous écrivons une fonction, nous l’écrivons pour satisfaire une exigence explicite ou implicite. La mise en œuvre reflète une compréhension de cette exigence, qui comprend en partie ce que nous attendons comme entrées de la fonction et comment nous nous attendons à ce que la fonction se comporte avec ces entrées. Par conséquent, nous testons le résultat de la fonction en fonction des entrées attendues, que le résultat de la fonction soit une valeur de retour ou un changement d'état. De plus, si l'unité dépend d'autres fonctions ou services, nous nous attendons également à ce qu'ils se comportent correctement et écrivons un test avec cette hypothèse implicite.
Il est beaucoup plus difficile de vérifier le comportement de l'unité dans des conditions anormales. Cela nécessite de déterminer ce qu'est une condition anormale, ce qui n'est généralement pas évident en inspectant le code. Ceci est rendu plus compliqué lorsque vous testez une unité dépendante - une unité qui s'attend à ce qu'une autre fonction ou un autre service se comporte correctement. De plus, nous ne savons pas comment un autre programmeur ou utilisateur pourrait exercer l’unité..
Le test unitaire ne remplace pas les autres pratiques de test; il devrait compléter d'autres pratiques de test, en fournissant un support de documentation supplémentaire et de la confiance. La figure 1 illustre un concept du "flux de développement d'application" - comment d'autres tests s'intègrent aux tests unitaires. Notez que le client peut être impliqué dans toutes les étapes, mais généralement aux étapes de la procédure de test de réception (ATP), de l'intégration du système et de la facilité d'utilisation..
Comparez cela avec le modèle en V du processus de développement et de test du logiciel. Bien qu’il soit lié au modèle de développement logiciel en cascade (qui, finalement, tous les autres modèles de développement logiciel sont un sous-ensemble ou une extension de), le modèle V fournit une bonne image du type de test requis pour chaque couche de le processus de développement logiciel:
Le modèle de test en VDe plus, lorsqu'un point de test échoue dans une autre pratique de test, un morceau de code spécifique peut généralement être identifié comme étant responsable de l'échec. Lorsque cela est le cas, il devient possible de traiter ce morceau de code comme une unité et d'écrire un test d'unité pour créer d'abord l'échec et, lorsque le code a été modifié, de vérifier le correctif.
Une procédure de test de réception (ATP) est souvent utilisée comme exigence contractuelle pour prouver que certaines fonctionnalités ont été implémentées. Les PTA sont souvent associés à des jalons, et les jalons sont souvent associés à des paiements ou à un financement de projet supplémentaire. Un ATP diffère d'un test unitaire, car il démontre que la fonctionnalité relative à l'exigence de tout élément de ligne a été implémentée. Par exemple, un test unitaire peut déterminer si le calcul est correct. Cependant, l'ATP peut valider que les éléments d'utilisateur sont fournis dans l'interface utilisateur et que l'interface utilisateur affiche le résultat du calcul tel que spécifié par l'exigence. Ces exigences ne sont pas couvertes par le test unitaire.
Un ATP peut initialement être écrit sous la forme d'une série d'interactions de l'interface utilisateur pour vérifier que les conditions requises ont été remplies. Les tests de régression de l'application au fur et à mesure de son évolution sont applicables aux tests unitaires ainsi qu'aux tests d'acceptation. Le test automatisé d'interface utilisateur est un autre outil complètement distinct du test unitaire qui permet de gagner du temps et de gagner du temps, tout en réduisant les erreurs de test. Comme pour les ATP, les tests unitaires ne remplacent en aucun cas la valeur des tests automatisés d'interface utilisateur..
Les tests unitaires, les ATP et les tests automatisés de l'interface utilisateur ne remplacent en aucun cas les tests de convivialité - plaçant l'application devant les utilisateurs et obtenant leur retour "d'expérience utilisateur". Les tests d'utilisabilité ne doivent pas consister à rechercher des défauts de calcul (bugs) et sont donc totalement hors de la portée des tests unitaires..
Certains outils de test unitaire fournissent un moyen de mesurer les performances d'une méthode. Par exemple, le moteur de test de Visual Studio indique le temps d'exécution et NUnit possède des attributs qui peuvent être utilisés pour vérifier qu'une méthode s'exécute dans le délai imparti..
Idéalement, un outil de test unitaire pour les langages .NET devrait implémenter explicitement les tests de performance pour compenser la compilation de code juste à temps (JIT) lors de la première exécution du code..
La plupart des tests de charge (et les tests de performance associés) ne conviennent pas aux tests unitaires. Certaines formes de tests de charge peuvent également être effectuées avec des tests unitaires, du moins dans les limites du matériel et du système d'exploitation, telles que:
Cependant, ces types de tests nécessitent idéalement la prise en charge de la structure ou de l'API du système d'exploitation pour simuler ces types de charges pour l'application testée. Forcer l'ensemble du système d'exploitation à consommer une grande quantité de mémoire, de ressources ou les deux affecte toutes les applications, y compris l'application de test unitaire. Ce n'est pas une approche souhaitable.
D'autres types de test de charge, tels que la simulation de plusieurs instances d'exécution simultanée d'une opération, ne sont pas candidats au test unitaire. Par exemple, il n'est probablement pas possible de tester les performances d'un service Web avec une charge d'un million de transactions par minute avec un seul ordinateur. Bien que ce type de test puisse être facilement écrit comme une unité, le test réel impliquerait une suite de machines de test. Et au final, vous n'avez testé qu'un comportement très étroit du service Web dans des conditions de réseau très spécifiques, qui ne représentent en aucun cas le monde réel..
Pour cette raison, les tests de performance et de charge ont une application limitée avec les tests unitaires.