TP : tests et bugs

Objectifs et prérequis

Prérequis : cours sur la mise au point des programmes

À l’issue de ce TP, vous saurez :

  • utiliser les exceptions pour rendre un programme robuste face aux saisies incorrectes ;
  • identifier et corriger des bogues dans un programme existant ;
  • écrire des tests unitaires avec assert et avec pytest ;
  • mesurer et comparer les performances d’un programme à l’aide de time.perf_counter().

Introduction

Les activités pratiques qui suivent ont pour objectif de vous faire appliquer les notions présentées dans le cours :

  • les exceptions ;
  • les bogues ;
  • les tests unitaires ;
  • les performances.

PizzaError

Une pizzeria souhaiterait créer une sorte de drive automatique pour ses clients afin qu’ils puissent commander en ligne une pizza selon les stocks disponibles.

Un stagiaire a déjà commencé l’écriture d’un programme prototype : pizza_drive.py .

Cependant comme vous allez pouvoir le constater, le programme est largement perfectible :

  • mauvaise gestion des saisies (des lettres au lieu d’un nombre) ;
  • commande de pizza malgré les stocks vides ;
  • plantage lorsque l’on commande la pizza 3.

À l’aide des exceptions, rendez ce programme plus robuste et plus ergonomique pour le client, c’est-à-dire sans plantage inopiné.

Indice

Provoquer chacun des trois problèmes et lire le nom de l’exception affichée : des lettres au lieu d’un nombre déclenchent ValueError, la pizza 3 IndexError. Une boucle while True peut alors redemander le choix tant qu’il n’est pas valide : try ... except ValueError autour de la conversion, puis un test sur le numéro et un autre sur le stock. Le squelette ci-dessous remplace la boucle while de la méthode prendre_la_commande.

Code à copier puis compléter
        while True:
            try:
                self.choix_client = int(input("Votre choix ? "))
            except ValueError:
                print("Veuillez saisir un nombre.")
                continue
            if self.choix_client not in (0, 1, 2):
                print("Ce numéro n'existe pas.")
            elif ...:
                print("Cette pizza n'est plus disponible.")
            else:
                break

Correction publiée après la séance.

Chasse aux bogues

  1. Téléchargez et décompressez l’archive bogues.zip .

  2. Pour chacun des scripts :

    • trouvez l’origine du bogue ;
    • proposez un correctif pour le rendre fonctionnel.
    Indice

    Exécuter le script et comparer le résultat obtenu avec le résultat attendu. En cas d’erreur, lire le message en partant de la dernière ligne : le type de l’erreur, puis la ligne en cause. Sinon, afficher les valeurs des variables avec print, ou suivre l’exécution pas à pas dans le débogueur de Thonny.

Correction publiée après la séance.

Tests unitaires

À votre tour de mettre en œuvre les tests unitaires sur une classe Rectangle à réaliser. Vous utiliserez la méthode simple avec le mot-clef assert, puis la bibliothèque pytest dans un second temps.

Classe Rectangle

L’entreprise GEOFORME développe des logiciels et outils de dessin industriel.

Le développeur avant vous dans le service a commencé à développer une classe Rectangle disponible dans le fichier rectangle.py . Il a principalement défini l’API de la classe et positionné des TODO comme ci-dessous dans chaque méthode :

# TODO : compléter le code de la méthode
pass

Si vous détectez des problèmes potentiels, vous pourrez les signaler en levant une Exception comme ci-dessous :

# Un problème potentiel...
if probleme == True:
    raise Exception("Un problème est survenu")

Le responsable du service vous demande de finir de coder cette classe et d’écrire les tests unitaires qui permettront de la valider et de l’intégrer dans le reste du logiciel de dessin industriel.

Concernant les tests unitaires, vous créerez deux versions et donc deux fichiers :

  • tests_rectangle_assert.py : version avec le mot-clef assert
  • tests_rectangle_pytest.py : version avec la bibliothèque pytest
Indice

Compléter d’abord le constructeur et les accesseurs, puis chaque méthode de calcul, en écrivant ses tests au fur et à mesure sur des rectangles dont on connaît les résultats de tête. Pour les problèmes potentiels, se demander quelles dimensions n’ont pas de sens pour un rectangle. Avec pytest, chaque test est une fonction dont le nom commence par test_, et la commande python -m pytest tests_rectangle_pytest.py les exécute toutes.

Code à copier puis compléter
import pytest
from rectangle import Rectangle

def test_aire():
    assert Rectangle(5, 3).calculer_aire() == ...

def test_dimension_negative():
    with pytest.raises(Exception):
        ...

Correction publiée après la séance.

Optimisation et performances

Chasse aux performances

Johnny Begood participe en ce moment à un challenge algorithmique sur une plate-forme en ligne. Pour le problème n°10, il doit écrire un programme qui affiche le \(2000\text{-ième}\) nombre premier. Il a écrit un code qui fonctionne parfaitement bien (le résultat est \(17~389\)), mais la plate-forme a fixé une limite de temps d’exécution (< 100 ms) pour valider le challenge et son programme est trop lent.

Chronométrage

  1. Téléchargez son code source : probleme_v00.py .

  2. À l’aide de la fonction perf_counter() du module time, mesurez et affichez le temps d’exécution de son programme et plus particulièrement de sa fonction rechercher_nieme_nombre_premier().

    Indice

    time.perf_counter() renvoie un instant en secondes : le relever juste avant et juste après l’appel de la fonction, puis afficher la différence, comme dans l’exemple du cours.

  3. Notez le résultat moyen obtenu sur trois exécutions et confirmez le refus par la plate-forme.

Première optimisation idiomatique

Johnny a entendu dire qu’en Python les boucles for étaient plus performantes que les boucles while.

  1. Créez un nouveau script probleme_v02.py, copiez-collez le code de la version v00 et modifiez le code de sa fonction etre_premier() pour remplacer la boucle while par une boucle for.

    Indice

    La boucle while fait prendre à facteur les valeurs 2, 3, …, n − 1 : quel range produit ces mêmes valeurs ? Les lignes facteur = 2 et facteur += 1 deviennent alors inutiles.

  2. Chronométrez le temps d’exécution obtenu et notez le résultat moyen sur trois lancements. Comparez le temps entre les versions v00 et v02. Est-ce suffisant ?

Seconde optimisation algorithmique

Après avoir lu la page Wikipédia consacrée aux nombres premiers, il découvre qu’un nombre N non premier possède toujours un diviseur compris entre 2 et \(\sqrt{N}\) : il suffit donc de tester les facteurs jusqu’à \(\sqrt{N}\).

  1. Créez un nouveau script probleme_v03.py à partir de la précédente version et modifiez la limite de la boucle for de \(n-1\) à \(\sqrt{n}\).

    Indice

    range n’accepte que des entiers : la borne s’écrit int(n ** 0.5) + 1, le + 1 permettant de tester aussi la racine elle-même, comme pour 25 = 5 × 5.

  2. Chronométrez cette nouvelle version comme précédemment et concluez.

Correction publiée après la séance.

Profilage

Effectuez un profilage du script de départ probleme_v00.py et notez les résultats :

  • pour la fonction rechercher_nieme_nombre_premier() :
    • nombre d’appels ;
    • temps moyen d’exécution ;
    • temps total d’exécution.
  • pour la fonction etre_premier() :
    • nombre d’appels ;
    • temps moyen d’exécution ;
    • temps total d’exécution.
Indice

Lancer python -m cProfile probleme_v00.py dans un terminal, depuis le dossier du script. Dans le tableau affiché, ncalls donne le nombre d’appels, percall le temps moyen d’un appel, et cumtime le temps total, y compris celui des fonctions appelées.

Correction publiée après la séance.