gwift-book/source/part-1-workspace/tools.adoc

14 KiB
Raw Blame History

Chaîne doutils

Tests

Comme tout bon framework qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier tests.py, qui inclut la classe TestCase depuis le package django.test:

from django.test import TestCase

class TestModel(TestCase):
    def test_str(self):
        raise NotImplementedError('Not implemented yet')

Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet disoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de lajout dune nouvelle fonctionnalité ou de la modification dune existante. Il existe plusieurs types de tests (intégration, comportement, …​); on ne parlera ici que des tests unitaires.

Avoir des tests, cest bien. Sassurer que tout est testé, cest mieux. Cest là quil est utile davoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.

Couverture de code

La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention quil ne sagit pas de vérifier que le code est bien testé, mais juste de vérifier quelle partie du code est testée. En Python, il existe le paquet coverage, qui se charge dévaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier requirements/base.txt, et lancez une couverture de code grâce à la commande coverage. La configuration peut se faire dans un fichier .coveragerc que vous placerez à la racine de votre projet, et qui sera lu lors de lexécution.

# requirements/base.text
[...]
coverage
django_coverage_plugin
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
    django_coverage_plugin

[report]
ignore_errors = True

[html]
directory = coverage_html_report
Note
le bloc ci-dessous est à revoir pour isoler la configuration.
$ coverage run --source "." manage.py test
$ coverage report

    Name                      Stmts   Miss  Cover
    ---------------------------------------------
    gwift\gwift\__init__.py       0      0   100%
    gwift\gwift\settings.py      17      0   100%
    gwift\gwift\urls.py           5      5     0%
    gwift\gwift\wsgi.py           4      4     0%
    gwift\manage.py               6      0   100%
    gwift\wish\__init__.py        0      0   100%
    gwift\wish\admin.py           1      0   100%
    gwift\wish\models.py         49     16    67%
    gwift\wish\tests.py           1      1     0%
    gwift\wish\views.py           6      6     0%
    ---------------------------------------------
    TOTAL                        89     32    64%
    ----

$ coverage html

Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, nhésitez pas à créer un fichier Makefile que vous placerez à la racine du projet. Lexemple ci-dessous permettra, grâce à la commande make coverage, darriver au même résultat que ci-dessus:

# Makefile for gwift
#

# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
    $(error The 'coverage' command was not found. Make sure you have coverage installed)
endif

.PHONY: help coverage

help:
    @echo "  coverage to run coverage check of the source files."

coverage:
    coverage run --source='.' manage.py test; coverage report; coverage html;
    @echo "Testing of coverage in the sources finished."

Complexité de McCabe

La complexité cyclomatique (ou complexité de McCabe) peut sapparenter à mesure de difficulté de compréhension du code, en fonction du nombre dembranchements trouvés dans une même section. Quand le cycle dexécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:

if True == True:
    pass # never happens

# continue ...

La condition existe, mais on ne passera jamais dedans. A linverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:

def compare(a, b, c, d, e):
    if a == b:
        if b == c:
            if c == d:
                if d == e:
                    print('Yeah!')
                    return 1

Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusquà arriver à limpression à lécran et à la valeur de retour. La complexité cyclomatique dun bloc est évaluée sur base du nombre dembranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.

Pour lexemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour sassurer que la couverture est complète. On devrait donc trouver:

  1. Un test pour entrer dans la condition a == b

  2. Un test pour entrer dans la condition b == c

  3. Un test pour entrer dans la condition c == d

  4. Un test pour entrer dans la condition d == e

  5. Et sassurer que nimporte quel autre cas retournera la valeur None.

On a donc bien besoin de minimum cinq tests pour couvrir lentièreté des cas présentés.

Le nombre de tests unitaires nécessaires à la couverture dun bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; dautres de 5.

Note
Evidemment, si on refactorise un bloc pour en extraire une méthode, cela naméliorera pas sa complexité cyclomatique globale

A nouveau, un greffon pour flake8 existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec pip install mccabe, et activez-le avec le paramètre --max-complexity. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.

Documentation

Il existe plusieurs manières de générer la documentation dun projet. Les plus connues sont Sphinx et MkDocs. Le premier a lavantage dêtre plus reconnu dans la communauté Python que lautre, de pouvoir parser le code pour en extraire la documentation et de pouvoir lancer des tests orientés documentation. A contrario, votre syntaxe devra respecter ReStructuredText. Le second a lavantage davoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans ses résultats.

Note
parler aussi dasciidoctor (même si moins bien intégré).

Dans limmédiat, nous nous contenterons davoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/…​). Dans la continuié de Flake8, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.

Note
voir si il ne faudrait pas mieux passer par pydocstyle.
$ pip install flake8_docstrings

Lancez ensuite flake8 avec la commande flake8 . --exclude="migrations". Sur notre projet (presque) vide, le résultat sera le suivant:

$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100  Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100  Missing docstring in public module
.\src\gwift\urls.py:1:1: D400  First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100  Missing docstring in public module
.\src\wish\admin.py:1:1: D100  Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100  Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100  Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100  Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused

Bref, on le voit: nous navons que très peu de modules, et aucun deux nest commenté.

En plus de cette méthode, Django permet également de rendre la documentation accessible depuis son interface dadministration.

pep8, flake8, pylint

Un outil existe et listera lensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour linstaller, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (., le nom dun répertoire, le nom dun fichier .py, …​).

Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez pyflakes: il analaysera vos sources à la recherche derreurs (imports inutilsés, méthodes inconnues, etc.).

Et finalement, si vous voulez grouper les deux, il existe flake8. Sur base la même interface que pep8, vous aurez en plus des avertissements concernant le code source.

from datetime import datetime

"""On stocke la date du jour dans la variable ToD4y"""

ToD4y = datetime.today()

def print_today(ToD4y):
    today = ToD4y
    print(ToD4y)

def GetToday():
    return ToD4y


if __name__ == "__main__":
    t =   Get_Today()
    print(t)
Note
lexemple est sans doute un peu trop tiré par les cheveux…

Lexécution de la commande flake8 . retourne ceci:

test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file

On trouve des erreurs:

  • de conventions: le nombre de lignes qui séparent deux fonctions, le nombre despace après un opérateur, une ligne vide à la fin du fichier, …​ Ces erreurs nen sont pas vraiment, elles indiquent juste de potentiels problèmes de communication si le code devait être lu ou compris par une autre personne.

  • de définition: une variable assignée mais pas utilisée ou une lexème non trouvé. Cette dernière information indique clairement un bug potentiel.

Létape daprès consiste à invoquer pylint. Lui, il est directement moins conciliant:

$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
    t =   Get_Today()
      ^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)

--------------------------------------------------------------------
Your code has been rated at -5.45/10

En gros, jai programmé comme une grosse bouse anémique (et oui, le score dévaluation du code permet bien daller en négatif). En vrac, on trouve des problèmes liés:

  • au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)

  • à des variables non définies (E0602)

  • de la documentation manquante (C0114, C0116)

  • de la redéfinition de variables (W0621).

Pour reprendre la documentation, chaque code possède sa signification (ouf!):

  • C convention related checks

  • R refactoring related checks

  • W various warnings

  • E errors, for probable bugs in the code

  • F fatal, if an error occurred which prevented pylint from doing further* processing.

PyLint est la version ++, pour ceux qui veulent un code propre et sans bavure.

Black

By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project youre reading. Formatting becomes transparent after a while and you can focus on the content instead.

Une chose qui fonctionne bien avec le langage Go, cest que les outils de base sont intégrés au compilateur : le formatage de code et les tests unitaires sont à la portée de tout le monde au travers de deux commandes simples :

  1. go fmt

  2. go test

En Python, cest plus complexe que cela, puisquil nexiste pas une manière unique darriver à un résultat (on la vu ci-dessus, rien que pour les tests, on a au moins deux librairies…).

Pour revenir à Go : est-ce que ce formatage est idéal et accepté par tout le monde ? Non.

Black fait le même travail: il arrive à un compromis entre la clarté du code, la facilité dinstallation et dintégration et un résultat. Ce résultat ne sera pas parfait, mais il conviendra dans 97,83% des cas (au moins).

pytest

mypy

Towncrier

voir ici