Moyen efficace de tester le code avec unittest

Python POO

En pratique, lorsque vous développez, si vous n’écrivez pas votre test unitaire avant ou juste après avoir écrit du code, vous n’allez pas l’écrire. Et donc vous vous retrouvez avec un code fragile. Ce n’est pas testable et contient probablement un bug.

Djibril Dassebe https://example.com/norajones
2022-11-12

Le plus souvent, nous testons nos codes au fur et à mesure que nous les écrivons, et cela s’appelle généralement Unit Testing. Nous voulons connaître, vérifier et valider de petits morceaux, des fonctions individuelles essentiellement, nous voulons les valider, morceau par morceau et nous assurer qu’ils fonctionnent correctement, pas seulement avec ce que nous appelons souvent happy path, c’est-à-dire des valeurs attendues, mais avec cas extrêmes. Que se passe-t-il si je passe un nombre négatif? Un zéro? Un None? un string vide ? etc. Tous ceux-ci sont considérés comme des cas extrêmes, nous devons donc tous les considérer et les tester pour pouvoir écrire des tests manuels. Un autre type de test est appelé test d’intégration et encore une fois, nous écrivons généralement des tests reproductibles même pour les tests d’intégration. Je vais me concentrer sur le code lié au projet suivant (juste à titre d’exemple et de pratique mais vous n’avez pas besoin de parcourir tout le projet et pour ceux qui sont intéressés, voici le lien [https://github.com/dassebedjibril/Bank-Account-Project] du projet ainsi que le code):

Comme vous pouvez le voir sur le code, le solde initial devrait être de 100, puis un acompte de 150,02. Nous affichons donc à nouveau le solde. Ensuite, nous allons faire un retrait de 0,02, nous fixons ensuite le taux d’intérêt à 1% et ainsi de suite. Jusqu’à présent, tout semble bien marcher et c’est compréhensible à partir de la sortie que nous avons en dessous. Enfin, nous allons en essayer un qui devrait nous donner un rejet (c’est-à-dire X selon la description) en retirant 1000 ce qui est supérieur à notre solde actuel252,5. ( dépôt → D, retrait → W, intérêt → I, rejeté →X) Mais bien sûr, ce n’est qu’un happy path. Que se passe-t-il si nous spécifions d’autres arguments qui ne fonctionnent pas comme prévu? C’est en quelque sorte le cas idéal où tout est correct. De toute évidence, nous devons tester bien plus que cela. L’autre problème avec cette approche, comme nous l’avons vu, est que nous devions l’écrire à la main et ensuite nous devions inspecter les résultats à la main.

Supposons que je trouve un bug dans ma classe ou que j’ai besoin d’ajouter des fonctionnalités ou que je décide que je veux refactoriser quelques éléments pour nettoyer ma classe. Il y a beaucoup de possibilités d’où je veux faire des changements dans la classe. Maintenant, je dois refaire ces tests. Donc tout ce travail que nous venons de faire, nous devons le refaire. Et bien sûr, la quantité de travail pour vraiment tester cette classe sera bien plus que cette dernière. Nous voulons donc vraiment écrire des tests automatisés. Nous voulons écrire des tests reproductibles, puis chaque fois que nous modifions notre code de base, nous pouvons réexécuter les mêmes tests et nous pouvons simplement nous assurer qu’ils réussissent tous et nous ne voulons pas avoir à faire d’inspection visuelle.

Il existe donc différents frameworks pour effectuer des tests, que vous examiniez des tests d’intégration, que vous ayez des frameworks comme behavior, par exemple, ou que vous examiniez des tests unitaires (Unit Testing). Le test unitaire est le premier type d’étape dans la méthodologie de test. Avec le test unitaire, vous testez quelque chose de très spécifique, vous avez peut-être une fonction qui valide qu’un nombre est un nombre réel et a une certaine valeur minimale. Nous écrirons un test unitaire pour cette fonction qui passera par un tas de valeurs différentes et nous assurerons que nous obtenons les résultats attendus.

Examinons donc la fonction suivante:

Nous écrirons donc un unit test qui appellerait cette fonction avec différentes valeurs et différentes (valeurs min) et lorsque nous écrivons le test, nous spécifions les valeurs afin que nous sachions quel devrait être le résultat. Dans certains cas, nous devrions obtenir une exception d’erreur de valeur. Dans d’autres cas, nous devrions simplement obtenir la valeur return car elle passe le test ou la validation et dans notre test, nous allons vérifier cela.

Nous allons vérifier que si nous passons cette valeur, cela génère-t-il une erreur de valeur? Ou si nous passons cette valeur min, renvoie-t-elle la valeur? Il est donc très autonome. Nous ne cherchons pas à valider le nombre réel utilisé ailleurs dans la classe, nous le regardons isolément et c’est cela que nous appelons unit test. Ils examinent vos différentes méthodes isolément les unes des autres.

Dans de nombreux cas, lorsque nous écrivons le code, les tests représentent plus de travail que l’écriture du code en lui-même. C’est donc une bonne idée d’écrire les tests au fur et à mesure que nous écrivons notre code. Certaines personnes aiment faire le contraire. Donc, ils écrivent d’abord les tests unitaires, ils s’assurent que les tests échouent parce que la fonction n’a pas été implémentée, puis ils vont écrire la fonction, puis relancent les tests et voient si elle fonctionne sans erreur. C’est donc quelque chose qui tombe davantage dans le camp du développement piloté par les tests.

L’autre approche est que vous écrivez votre fonction et que vous arrêtez d’écrire votre code principal à ce moment-là, vous vous arrêtez et vous allez à vos tests unitaires et vous écrivez les tests unitaires pour cette fonction, puis vous revenez en arrière et vous continuez à écrire le code, vous vous arrêtez, vous écrivez les tests unitaires et vous faites des tours entre les deux, si vous faites cela, il y a de fortes chances que vous écriviez des tests unitaires pour votre code.

Normalement, les unit test sont appelés à partir de la ligne de commande, qui à son tour configure un lanceur de tests et exécute nos tests de manière transparente. Dans ce cas, afin que nous puissions rester dans un Jupyter Notebook, nous devrons ajouter quelques lignes de code supplémentaires pour configurer manuellement un testeur. Ce n’est généralement pas nécessaire.

Habituellement, nous exécutons les tests unitaires depuis l’extérieur du Notebook afin que vous écriviez un fichier ou un groupe de fichiers, de modules et de packages. Vous écririez votre application, vous écririez vos tests, qui ne sont eux-mêmes que des fichiers Python essentiellement qui pourraient être organisés en modules et packages, puis vous exécutez le test unitaire de l’extérieur et il collectera essentiellement toutes les méthodes qui sont définies dans ces packages qui commencent par le mot test et il sait qu’il s’agit d’un test et qu’il va l’exécuter, puis il vous donnera le résultat qui marche ou pas. Mais we dois le faire à l’intérieur du notebook, nous allons faire les choses un peu différemment, mais c’est essentiellement la même idée. Rien ne change.

Nous allons donc écrire cette petite fonction appelée run_tests, et nous passerons une classe. Nous utilisons une classe pour définir nos tests et nous combinons tous les tests dans une classe et nous pouvons avoir plusieurs classes pour couvrir différentes parties de notre application. Nous allons donc ici exécuter les tests qui sont contenus dans une seule classe.

Écrivons quelques unit test.

Pour ce faire, nous allons créer une classe appelée TestAccount() et elle va essentiellement contenir tous les tests que nous allons définir et nous allons hériter du test unitaire. Ce qui se passe essentiellement, c’est que nous héritons de certaines fonctionnalités de base. Un test unitaire est essentiellement une fonction, une méthode, une instance dans cette classe et il y a très souvent des cas où vous voulez que certaines fonctions calculent quelque chose ou fassent quelque chose dont vous avez besoin, mais vous ne voulez pas que cela soit exécuté comme un test donc, vous ne le préfixerez pas avec le mot test. Dans notre cas, nous voulons tester. Voyons comment certains tests simples sont configurés et exécutés:

Nous maintenant pouvons écrire quelques tests unitaires simples. La seule chose est que chaque test unitaire doit être une fonction dans la classe qui commence par le mot «test» de cette façon, il est automatiquement identifié comme un test unitaire. Nous avons également la possibilité de définir les fonctionnalités de configuration et de démontage. Ce ne sont que des méthodes qui seront exécutées avant chaque méthode de test et juste après. Alors permettez-moi de vous montrer comment cela fonctionne, ce que nous devons faire est de définir une méthode de configuration qui est essentiellement une méthode que notre test va comprendre, et c’est une instance de la méthode. Voici un exemple simple qui montre comment cela fonctionne:

Maintenant, si nous le voulons, nous pouvons également utilser tearDown. Supposons que vous ouvriez une connexion à une base de données, que vous souhaitiez peut-être tester en utilisant des données réelles dans une base de données ou que vous deviez appeler un API externe. Nous mettons en place un tearDown avant et après chaque essai.

Même si le test échoue, la méthode de tearDown fonctionnera toujours:

Nous pourrions donc utiliser setUp pour peut-être créer des comptes bancaires que nous pourrons utiliser tout au long de nos tests. N’oubliez pas que TestAccount est une classe, nous pouvons donc créer des attributs d’instance dans la méthode setUp et y accéder dans l’une des méthodes d’instance (comme les méthodes de test). Une autre chose à surveiller est qu’il n’y a aucune garantie de l’ordre dans lequel les tests unitaires sont exécutés. La meilleure pratique est que les tests unitaires doivent être indépendants les uns des autres. Ajoutons d’abord quelques unit test simples pour la classe TimeZone:

Remarquez comment nous devions exécuter plusieurs scénarios pour tester des fuseaux horaires (Time zone) non égaux. C’est un événement assez courant, et il existe une meilleure façon de le configurer afin que nous ayons en fait des tests séparés, qui se distinguent les uns des autres (c’est un peu plus facile avec l’utilisation de pytest, mais le résultat final est similaire):

Là où cela pourrait être utile, c’est dans le cas d’un échec de test:

Comme vous pouvez le voir, nous avons un message associé au test qui a eu un echec. Revenons en arrière et supprimons ce test incorrect:

Nous pouvons maintenant commencer à ajouter des unit test supplémentaires pour notre classe Account. N’oubliez pas que les tests unitaires sont destinés à tester une fonctionnalité spécifique, n’essayez pas de trop regrouper vos tests, sinon les messages d’erreur peuvent devenir moins significatifs, ce qui rend plus difficile la recherche du problème réel. Une pratique recommandée consiste soit à configurer des tests unitaires avant d’écrire votre code, soit peu de temps après.
Ecrivons quelques tests pour la classe Account:

Tout s’est bien passé. Maintenant, si nous modifions le code, nous pouvons simplement relancer le test et nous pouvons nous assurer que nous n’avons rien changé ou bouleversé de ce que nous essayions de réparer. `Cela nous donne une idée de la façon de faire avec les tests unitaires lorsque les choses fonctionnent comme prévu.

Un dernier élément de la fonctionnalité de test unitaire consiste à gérer les exceptions lorsqu’elles sont attendues, par exemple la création d’un compte avec un prénom vide devrait entraîner une exception ValueError. Nous pouvons écrire un test unitaire qui testera cette exception attendue, et qui échouera si l’exception n’est pas rencontrée (ou est une exception différente). Pour ce faire, nous devons indiquer qu’une exception est attendue, ainsi que la classe d’exception attendue.

Alors maintenant, nous nous attendons à ce que le code ci-dessus donne une erreur de ValueError et effectivement le test passe. Maintenant, si nous attendions un ValueError, alors nous verrons que nous obtenons un échec indiquant que le prénom ne peut pas être vide. Nous avons donc une erreur de valeur ValueError.

La même situation se produit lorsque vous essayez de retirer plus que ce que nous avons (ou à découvert)

Nous n’avons pas d’exception! C’est un bug dans notre code. Nous allons donc le corriger et refaire les tests. Pour éviter d’appeler les variables comme ça à chaque fois que nous en avons besoin, nous pouvons simplement écrire une méthode setUp à partir de notre classe TestAccount et les appeler avec .self chaque fois que nous en avons besoin. Parce qu’à chaque fois qu’un test est lancé, il exécute d’abord la configuration. Nous n’avons pas à nous soucier de modifier les valeurs des instances actuelles car elles seront réinitialisées.

Je vais m’arrêter ici, j’espère que cela vous a été utile. Ce n’est peut-être pas très clair de cette façon, mais cela aurait été mieux avec une vidéo.

Merci pour la lecture.