Working on tests

This commit is contained in:
Fred 2021-03-28 17:44:41 +02:00
parent 16d0a344bb
commit 831fb93783
4 changed files with 100 additions and 24 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1 @@
<mxfile host="Electron" modified="2021-01-23T10:14:26.227Z" agent="5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.1.8 Chrome/87.0.4280.88 Electron/11.1.1 Safari/537.36" etag="Aidg1-D2vaUEe86Td55A" version="14.1.8" type="device"><diagram id="D_iwcrbJ42AVik1I7Eo6" name="Page-1">3Vpbc6s2EP41TJ/CIC42foydy2mbdjJNb6cvGRlkUI9AHiF8ya8/EkgGLF8b23H6kqAFLdLuft/uClveKFs8MjhNf6ExIpbrxAvLu7Nc1w9c8VcKlrUg8LxakDAc1yLQCF7wG1JCR0lLHKOi8yCnlHA87Qojmuco4h0ZZIzOu49NKOm+dQoTZAheIkhM6V845mktDd1+I/+CcJLqN4PeoL6TQf2w2kmRwpjOWyLv3vJGjFJeX2WLESLSdtou9byHLXdXC2Mo54dM+JPd/EhBePPzP0/BY3r37NCfyA1Q7in4Uu8YxcIAakgZT2lCc0juG+kwKtkMSa1ADBgt87gaOWJUfEM8StWtZvYTpVMl/BdxvlR+hiWnQpTyjKi7aIH531KXHQRq+LUaDvTwbqFeVQ2WrcEzYjhDHDEtyzlb1sr6gR5/bd9sdFUjraw2ibTDVlMrUUFLFqEd9g1UyEKWIL7LD94qIgSSEBX7YEsxkSECOZ51FwJVTCer51ZTnykWS3QdBT83UMGn0Ad8p6uiXpia1QSPuGgtoxFVIXVEeKntzyAp1RbMcCNEYFmGlQDIVAojQkuhdDhPMUcvU1gZeC7YpRsqTeCB9cBTb0WMo8VuF5oW15brdy2nUTxvSABoWdoigNDZ7qOOdY81pSbEli1TziUL3krF7oNt29eF5RPCqH8gjIKdPnVsxwFdPCij/leY6UfoZFKgsyCovx9BU7kYxO5nwqyFhoNONtKRMSzSlVdbDio4o9/QiBIqKTOnuQyICSZEiyzX8z3f9yV7wmJa59cJXkhlQwLHiDzTAnNMcwlblFfcO5S4wyKDPq09MKac06z1wC3BibzBZUANoRqt9NCSE5yL9enM7rRIIlskst6wZ7PYnqPx61im+mraFl5wTsMLYI1Rfdd2A4Magg3M4AV2cCZuGHw2amil+U6WPz7Jn5BkwgNJpr+PZIKw/8lYJjQC6A+OCS4gRyUzQkdjsMzIbcTpOzDfComNaD9vjl+DsmemeG8Djntny/BgP9d3YbinQjq1vfy1YrK/wWBhz7TYCgCnN1nwGXgN7Ga1pk35qC4FeAdSH/A3B8jBzPY+b3v7AWKUKdNWwlC9RiuHtJ1nFCDbi6hCYA7nye9VWHhCgLPqHEH/v8NZIrZI8Fj8hZE0zWuMGZJsKezwMBHXc0iIXcyS87cyTtjFrb+J6HRx0gZucC7cuoP/EW5BB7UNiC+AW/9Q3A4+FLe+gdtfE5wvriu7gUG4KuV107+hJNhY2p8NJ9d7PPfu/AYuiJPe58BJz8DJSyny1wwXlMXXhRYPrBfPngGV8KJQ6V0pVI5ocE8Z8ocemW3pZi8T8a55vPUb4lT2nI418qxbES09IpY/HAtRL5FX5IdyR28qjVc1oXtqu/V+NMNxXIUEQwV+g+NKn/RJdbpW7TsYWsHdLvSob1BqsrU6M277b0fs7jhS8F1/7eBSRfs7vw8Axx50zypuXGD33a6e8507gAOONz+2KHDXPgOEJs31LklzwDyquTKLeWsWG3y0xczT0ccyxxFl+ZVZzt+fUi9qOfeAz3dGv90ykEmdBg3rvnlCc65SKXC39dFvJUNySwnKEYNi+Q8vsjiK0OsXBAlPL9JNe30gPyd3HKW1tBtqYIfhBX2lFbd89aAOGQyfic1z67gvQUp0eBrdhJ0dBdYJHON6g26KDM3mzd8AH/d4l4hh87uNOhc2P37x7r8D</diagram></mxfile>

View File

@ -321,27 +321,79 @@ Comme tout bon *framework* qui se respecte, Django embarque tout un environnemen
----
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 d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
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 d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante.
Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
Avoir des tests, c'est bien.
S'assurer que tout est testé, c'est mieux.
C'est là qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré.
TODO: Vérifier comment les applications sont construites. Type DRF, Django Social Auth, tout ça.
Comme indiqué ci-dessus, Django propose son propre cadre de tests, au travers du package `django.tests`.
Une bonne pratique (parfois discutée) consiste cependant à switcher vers `pytest`, qui présente quelques avantages:
* Une syntaxe plus concise (au prix de https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html[quelques conventions], même si elles restent configurables): un test est une fonction, et ne doit pas obligatoirement faire partie d'une classe héritant de `TestCase` - la seule nécessité étant que cette fonction fasse partie d'un module commençant ou finissant par "test" (`test_example.py` ou `example_test.py`).
* Une compatibilité avec du code Python "classique" - vous ne devrez donc retenir qu'un seul ensemble de commandes ;-)
* Des _fixtures_ faciles à réutiliser entre vos différents composants
* Une compatibilité avec le reste de l'écosystème, dont la couverture de code présentée ci-dessous.
Ainsi, après installation, il nous suffit de créer notre module `test_models.py`, dans lequel nous allons simplement tester l'addition d'un nombre et d'une chaîne de caractères (oui, c'est complètement biesse; on est sur la partie théorique ici):
[source,python]
----
def test_add():
assert 1 + 1 == "argh"
----
Forcément, cela va planter.
Pour nous en assurer (dès fois que quelqu'un en doute), il nous suffit de démarrer la commande `pytest`:
[source,bash]
----
λ pytest
============================= test session starts ====================================
platform ...
rootdir: ...
plugins: django-4.1.0
collected 1 item
gwift\test_models.py F [100%]
================================== FAILURES ==========================================
_______________________________ test_basic_add _______________________________________
def test_basic_add():
> assert 1 + 1 == "argh"
E AssertionError: assert (1 + 1) == 'argh'
gwift\test_models.py:2: AssertionError
=========================== short test summary info ==================================
FAILED gwift/test_models.py::test_basic_add - AssertionError: assert (1 + 1) == 'argh'
============================== 1 failed in 0.10s =====================================
----
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit 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 https://pypi.python.org/pypi/coverage/[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 l'exécution.
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests.
Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée.
Le paquet `coverage` se charge d'évaluer le pourcentage de code couvert par les tests.
Avec `pytest`, il convient d'utiliser le paquet https://pypi.org/project/pytest-cov/[`pytest-cov`], suivi de la commande `pytest --cov=gwift tests/`.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez passer par le paquet https://pypi.org/project/django-coverage-plugin/[django-coverage-plugin] 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 l'exécution.
[source,bash]
----
# requirements/base.text
[...]
coverage
django_coverage_plugin
----
@ -386,6 +438,8 @@ $ coverage report
$ coverage html
----
<--- / partie obsolète --->
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes.
@ -402,7 +456,7 @@ L'outil le plus connu est https://tox.readthedocs.io/en/latest/[Tox], qui consis
----
# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py27,py36
envlist = py36,py37,py38,py39
[testenv]
deps =

View File

@ -6,9 +6,9 @@ Il est du coup probable d'oublier une partie des désidérata, de zapper une fon
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes.
Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash.
Le serveur que django met à notre disposition _via_ la commande `runserver` est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...).
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois. En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanément... En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
De même, Django propose par défaut une base de données SQLite, qui fonctionne parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois. En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanément...
En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce dont vous aurez besoin au final.
Dans cette partie, nous aborderons les points suivants:
@ -18,20 +18,48 @@ Dans cette partie, nous aborderons les points suivants:
* Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Rendre notre application accessible depuis l'extérieur.
== Infrastructure
== Infrastructure & composants
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, nous devrions arriver à quelque chose de synthéthique:
Pour une mise ne production, le standard _de facto_ est le suivant:
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
* Nginx comme reverse proxy
* HAProxy pour la distribution de charge
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisor pour le monitoring
* PostgreSQL ou MariaDB comme base de données.
* Celery et RabbitMQ pour l'exécution de tâches asynchrones
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante:
. L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. Le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. Le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. La requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. Elle est ensuite transmise par socket et est prise en compte par un des _workers_ (= un processus Python) instancié par Gunicorn. Si l'un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord.
. Qui la transmet ensuite à l'un de ses _workers_ (= un processus Python).
. Après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
=== Reverse proxy
Le principe du *proxy inverse* est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système.
Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats footnote:[https://fr.wikipedia.org/wiki/Proxy_inverse]
=== Load balancer
=== Workers
=== Supervision des processus
=== Base de données
=== Tâches asynchrones
=== Mise en cache
== Code source
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
@ -47,14 +75,7 @@ Il est possible de démarrer petit, et de suivre l'évolution des besoins en fon
== Outils de supervision et de mise à disposition
Pour une mise ne production, le standard _de facto_ est le suivant:
* Nginx comme reverse proxy
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisorctl pour le monitoring
* PostgreSQL ou MariaDB comme base de données.
* Celery et RabbitMQ pour l'exécution de tâches asynchrones
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
== Méthode de déploiement