• 4 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 07/10/2017

Finalisez les tests et améliorez votre couverture de tests

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

 

 

Dans ce chapitre nous allons terminer de tester notre projet ! Youhou !  🚀

Revenons au fichier de tests que nous avons volontairement laissé en suspens :  test_world.py . Pour rappel, voici ce qu'il nous reste à tester : 

# - AgreeablenessGraph :
#   - récupérer un titre
#   - récupérer x_label
#   - récupérer y_label
#   - récupérer xy_values sous forme de tuples
#   - la première valeur de xy_values est la densité de population moyenne pour la première zone
#   - la seconde valeur de xy_values est l'agréabilité moyenne

 

Ecrire les trois premiers tests est, pour le moment, assez facile. Vous êtes des pros maintenant ! 

class TestAgreeablenessGraph:
# - AgreeablenessGraph :
    GRAPH = script.AgreeablenessGraph()

#   - récupérer un titre
    def test_title(self):
        assert self.GRAPH.title == 'Nice people live in the countryside'

#   - récupérer x_label
    def test_x_label(self):
        assert self.GRAPH.x_label == 'population density'

#   - récupérer y_label
    def test_y_label(self):
        assert self.GRAPH.y_label == 'agreeableness'

 

Mais les trois tests suivants sont bien plus compliqués qu'ils n'en ont l'air. Afin de pouvoir véritablement tester les graphiques, nous avons besoin de :

  • créer toutes les zones

  • ajouter au moins 10 habitants afin de vérifier que la densité de population et l'agréabilité moyenne sont bien calculées.

Nous voyons également que ces opérations se répéteront pour nos trois derniers tests et cela serait redondant. Comment faire ? Laissez-moi vous présenter les méthodes  setup()  et  teardown()  !

 

Exécuter des actions avant ou après des tests

 Pytest vous permet d'exécuter des instructions avant ou après chaque test unitaire. Cette fonctionnalité de notre librairie de tests porte le doux nom de setup et teardown.

  • La méthode  setup_function()  est déclenchée avant votre test unitaire.

  • La méthode  teardown_function()  est déclenchée à la fin de chaque test unitaire.

Exemple :

def setup_function(function):
    """ setup any state tied to the execution of the given function.
    Invoked for every test function in the module.
    """
    print("before: ", function)

def teardown_function(function):
    """ teardown any state that was previously setup with a setup_function
    call.
    """
    print("after:", function)

Il existe plusieurs "niveaux" : déclenchement avant chaque test unitaire, à la création d'une classe ou à l'import du module pytest.

  • setup_module(module): fonction appelée lors de l'import d'un module.

  • setup_class(cls): fonction appelée lors de la création de la classe.

  • setup_method(self, method): fonction appelée lors du lancement d'un test unitaire faisant partie d'une classe.

  • setup_function(function): fonction appelée lors du lancement d'un test unitaire.

 

Je vous propose d'utiliser cette fonctionnalité dans nos tests.

Le setup servira à :

  • créer toutes les zones

  • créer les variables de classe

  • ajouter des habitants

 

class TestAgreeablenessGraph:
# - AgreeablenessGraph :

    @classmethod
    def setup_class(cls):
        script.Zone._initialize_zones()
        cls.ZONE = script.Zone.ZONES[0]
        cls.GRAPH = script.AgreeablenessGraph()
        cls.ZONES = script.Zone.ZONES
        for _ in range(0, 10):
            cls.ZONE.add_inhabitant(script.Agent(script.Position(-180, -89), agreeableness=1))

#   - récupérer un titre
    def test_title(self):
        assert self.GRAPH.title == 'Nice people live in the countryside'

#   - récupérer x_label
    def test_x_label(self):
        assert self.GRAPH.x_label == 'population density'

#   - récupérer y_label
    def test_y_label(self):
        assert self.GRAPH.y_label == 'agreeableness'

#   - récupérer xy_values sous forme de tuples
    def test_xy_values(self):
        assert len(self.GRAPH.xy_values(self.ZONES)) == 2

#   - la première valeur de xy_values est la densité de population moyenne pour la première zone
    def test_average_population_density(self):
        assert self.GRAPH.xy_values(self.ZONES)[0][0] == self.ZONE.population_density()

#   - la seconde valeur de xy_values est l'agréabilité moyenne
    def test_average_agreeableness(self):
        assert self.GRAPH.xy_values(self.ZONES)[1][0] == self.ZONE.average_agreeableness()

 

Faire le ménage

A présent, revenons aux tests de la classe TestZone car un élément me chagrine.

Le test  test_get_zones  fonctionne plutôt bien. Pourtant, à aucun moment nous ne créons de zone...

def test_get_zones(self):
    assert len(script.Zone.ZONES) == 64800

Normalement, chaque test unitaire est censé être tout à fait aveugle. Il se connait lui-même ainsi que sa classe mais rien d'autre. Et voilà que ce test-là valide que des zones ont déjà été créées !

Avez-vous trouvé pourquoi ?

Le test précédent,  test_find_zone_that_contains, appelle la fonction  find_zone_that_contains. Si nous regardons notre code, nous nous apercevons que cette fonction crée des zones si aucune zone n'existe pour le moment. Pour être précise, elle modifie un attribut de classe !

Or, chacun de nos tests accède aux attributs de classe. Nous pouvons donc dire que nous ne respectons pas réellement le principe d'indépendance de chacun de nos tests unitaires.

Cela risque de nous poser problème si nous utilisons fréquemment ces zones créées car chaque test interagit avec le même objet ! Il nous faut donc corriger cela très vite.

Je vous propose de ne plus utiliser de variable de classe du tout afin de respecter ce principe d'indépendance. A la place, nous créerons des attributs d'instance.

Le  teardown  nous sert, lui, à vider la zone de tout habitant.

class TestZone:

    def setup_method(self):
        self.position1 = script.Position(100, 33)
        self.position2 = script.Position(101, 34)  
        self.zone = script.Zone(self.position1, self.position2)
        script.Zone._initialize_zones()
        agent = script.Agent(self.position1, agreeableness=1)
        self.zone.inhabitants = [agent]

    def teardown_method(self):
        script.Zone.ZONES = []

Pour vérifier que cela fonctionne, nous déplacons le testtest_get_zones()en premier :

class TestZone:
    ...

    def teardown_method(self):
        script.Zone.ZONES = []

    #   - récupérer toutes les instances Zone (Zone.ZONES)
    # On devrait avoir exactement 64800 zones
    def test_get_zones(self):
        assert len(script.Zone.ZONES) == 64800

On lance... Et tout fonctionne !

Je vais faire les mêmes changements pour  Agent  et  Position. Si vous le faites en même temps que moi, vous verrez d'ailleurs que nous avons désormais une nouvelle erreur :

self = <test_world.TestAgent object at 0x10d51e0f0>

    def test_get_position(self):
>       assert self.AGENT.position == 5
E       assert 3 == 5
E        +  where 3 = <program.world.Agent object at 0x10d51e128>.position
E        +    where <program.world.Agent object at 0x10d51e128> = <test_world.TestAgent object at 0x10d51e0f0>.AGENT

Tiens, étonnant ! Allons voir notre code :

class TestAgent:
    def setup_method(self):
        self.agent = script.Agent(3)

    # - Agent : 
    #   - modifier un attribut position
    def test_set_position(self):
        self.AGENT.position = 5
        assert self.agent.position == 5

    #   - récupérer un attribut position
    def test_get_position(self):
        assert self.agent.position == 5

Alors, avez-vous trouvé l'erreur ? :)

Dans le chapitre précédent, chaque test utilisait le même objet : l'attribut de classe  AGENT  . Nous commencions par créer un attribut de classe AGENT qui avait une position à  3 .

Puis, dans  test_set_position(), nous changions cette position à  5.

C'est pourquoi le test suivant,  test_get_position(), n'affichait pas d'erreur.

Pour autant, il s'agit d'un mauvais signe car nos tests deviennent trop dépendants les uns des autres. Que se passe-t-il le jour où je change l'ordre de mes tests ? Ils échoueront même si ce que nous désirons tester est toujours juste !

Utiliser  setup_method()  est alors très pratique et vivement recommandé pour garantir que chaque test unitaire a uniquement les informations nécessaires à son bon fonctionnement.

Modifions notre test :

class TestAgent:
    def setup_method(self):
        self.agent = script.Agent(3)

    # - Agent : 
    #   - modifier un attribut position
    def test_set_position(self):
        self.AGENT.position = 5
        assert self.agent.position == 5

    #   - récupérer un attribut position
    def test_get_position(self):
        assert self.agent.position == 3

    #   - assigner un dictionnaire en tant qu'attributs
    def test_set_agent_attributes(self):
        agent = script.Agent(3, agreeableness=-1)
        assert agent.agreeableness == -1

Ils passent ! \o/

Avant de continuer notre aventure, et de créer notre première fonctionnalité, j'aimerais vous parler d'un concept assez particulier : la couverture de test.

 

Couverture de test

Une couverture de test, de son doux nom  code coverage  en anglais, nous permet de connaître le pourcentage de notre code qui est testé.

Cela nous donne une idée des parts d'ombre qui peuvent subsister dans notre projet ! :ninja:

Une bonne couverture de test, supérieure à 80%, est signe d'un projet bien testé et auquel il est plus facile d'ajouter de nouvelles fonctionnalités.

Vous vous demandez certainement : comment connaître le taux de couverture de notre projet ?

Rassurez-vous, nous n'allons pas parcourir notre code ligne après ligne pour répertorier ce qui est testé ! Une librairie Python, Coverage.py, fait justement cela pour vous ! :)

Commencez par installer la librairie en écrivant dans votre terminal  pip install coverage  puis  pip install pytest-cov.

Puis lancez la commande  pytest --cov=program --cov-report html test_*.py.

Celle-ci signifie : "teste les fichiers contenus dans le dossier 'program', crée un rapport en html et utilise les tests qui sont ici-même et qui sont de la forme 'test_[caractères].py'".

Quand le script est terminé, vous découvrez qu'un nouveau dossier a été créé à l'endroit où vous avez lancé la commande.  htmlcov  contient différents documents dont un fichier HTML.

En l'ouvrant, vous verrez le rapport de tests.  world.py  est testé à 81% et  download_agents.py  l'est à 82%.

Nous sommes en mesure de nous poser la question : pourquoi ? Coverage, donne-nous une explication !

Cela tombe bien car le module a justement créé deux nouveaux fichiers pour détailler son explication. Pour être précise, il a créé un fichier par fichier testé. Ouvrons  program_world.html.

Coverage a surligné en rouge le code qui n'est pas testé. En effet, nous n'avons pas testé la boucle  main  et quelques autres lignes. Nous avons encore du travail !

Je vous laisse continuer les tests ! Pour ma part, je vais commencer à implémenter la nouvelle fonctionnalité : un graphique qui me permettra de répondre à la question "les vieux sont-ils vraiment plus bougons que les jeunes ?".

Nous découvrons cela dans le chapitre suivant !

Code du chapitre

Retrouvez le code du chapitre sur Github !

Exemple de certificat de réussite
Exemple de certificat de réussite