Nous allons reprendre l’application du TP3 Kubernetes car c’est une application flask multicomposant simple. Cela nous permettra d’illustrer les différents types de tests logiciels.
Créez un nouveau dossier jenkins_TPs
sur le bureau (ou ailleurs) en clonant le projet de base avec git clone https://github.com/Uptime-Formation/corrections_tp.git -b jenkins_tp1_base tp_jenkins_application
.
Ouvrez le dossier jenkins_TPs/jenkins-k8s/tp1_monsterstack_testing
dans VSCode.
Dans le dossier src
(nom conventionnel pour le dossier du code source de l’application), créez le fichier monster_icon.py
avec à l’intérieur le code:
from flask import Flask, Response, request
from flask import jsonify
import requests
import hashlib
import redis
import socket
app = Flask(__name__)
redis_cache = redis.StrictRedis(host='redis', port=6379, socket_connect_timeout=2, socket_timeout=2, db=0)
salt = "UNIQUE_SALT"
default_name = 'John Doe'
page_template = '''
<html>
<head>
<title>Monster Icon</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.css">
</head>
<body style="text-align: center">
<h1>Monster Icon</h1>
<form method="POST">
<strong>Hello dear <input type="text" name="name" value="{name}">
<input type="submit" value="submit"> !
</form>
<div>
<h4>Here is your monster icon :</h4>
<img src="/monster/{name_hash}"/>
<div>
</br></br><h4> container info: </h4>
<ul>
<li>Hostname: {hostname}</li>
<li>Visits: {visits} </li>
</ul></strong>
</body>
</html>
'''
def render(page_template, values):
return page_template.format(**values)
def redis_visits_counter(redis_cache):
try:
visits = redis_cache.incr("counter")
except redis.RedisError:
visits = "<i>cannot connect to Redis, counter disabled</i>"
return int(visits)
def hash_name(name, salt):
salted_name = salt + name
name_hash = hashlib.sha256(salted_name.encode()).hexdigest()
return name_hash
@app.route('/', methods=['GET', 'POST'])
def mainpage():
name = request.form['name'] if request.method == 'POST' else default_name
values = {
'name': name,
'name_hash': hash_name(name, salt),
'visits': redis_visits_counter(redis_cache),
'hostname': socket.gethostname()
}
page = render(page_template, values)
return page
@app.route('/monster/<name>')
def get_identicon(name):
image = redis_cache.get(name)
if image is None:
print ("Cache miss: picture icon not found in Redis", flush=True)
r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
image = r.content
redis_cache.set(name, image)
return Response(image, mimetype='image/png')
@app.route('/healthz')
def healthz():
data = {'ready': 'true'}
return jsonify(data)
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
__init__.py
vide dans src
pour matérialiser la création d’un module applicatif.Observons notre application:
/
, /monster/<name>
, /healthz
.render
, hash_name
, redis_visits_counter
redis_visits_counter
fait appel a Redis et nécessite donc un redis fonctionnel pour être exécutéeNous aimerions maintenant tester notre application pour garantir qu’elle fonctionne au fur et a mesure du développement et qu’elle se déploie parfaitement en production.
le premier type de test applicatif classique est le test unitaire:
il s’agit de tester chaque fonction interne du programme pour s’assurer que le changement d’une fonction ne va pas “casser” un autre endroit du programme.
le framework de test le plus populaire en python
se nomme pytest
: documentation de pytest
C’est un exécuteur de tests qui cherche automatiquement toutes les fonctions commençant par test_
pour
assertion
lorsqu’elles échouent.Il fournit pleins d’outils pour réaliser des suites de tests complexes sans trop d’effort
Pour lancer les tests (en mode détaillé) on execute pytest --verbose <dossier ou fichier de test>
ou encore python -m pytest --verbose <dossier ou fichier de test>
.
Vérifiez que pytest
est bien dans le fichier requirements.txt
créez un virtualenv pour notre application avec virtualenv -p python3 venv
puis activez le avec source venv/bin/activate
installez les dépendances avec pip3 install -r requirements.txt
Nous allons maintenant l’utiliser pytest et écrire des tests unitaires :
tests
dans src
avec à l’intérieur des fichiers unit_tests.py
et __init__.py
vides.src
├── __init__.py
├── monster_icon.py
└── tests
├── __init__.py
└── unit_tests.py
unit_tests.py
:from ..monster_icon import <importer les fonctions à tester>
def test_render():
template = '''
<html>
<h1>{title}<h1>
visites: {visits}
</html>
'''
values = {
'title': 'Pytest!',
'visits': 32
}
result = render(template, values)
<assertions>
def test_hash_name():
name = "Jacques"
salt = "lesel"
salt2 = "leselbis"
result = hash_name(name, salt)
result2 = hash_name(name, salt2)
assert result
assert result != result2
Les tests sont généralement basés sur une série d’assertions c’est à dire de tests qui déclenchent une exception s’ils sont faux.
assert result
visits
à remplacée dans le template: assert str(values['visits']) in result
title
: assert values['title'] in result
Observons les tests unitaires: le principe est d’appeler les fonctions à tester à l’intérieur de la fonction de test et vérifier que le résultat est conforme à l’attendu. Ainsi si l’on modifie un fonction le test échouera.
Lancez les tests avec python -m pytest --verbose src/tests/unit_tests.py
ils devraient bien se dérouler.
Modifiez la fonction render
de monster_icon.py
en ajoutant:
for key, val in values.items():
values2 = {}
values2[key] = val+1 if isinstance(val, int) else val
return page_template.format(**values2)
Relancez les tests et constatez que notre test nous a prévenu que la fonction render
avait un comportement bizarre.
Corrigez à nouveau le code en enlevant les lignes précédemment ajoutées.
Les tests d’intégration sont des tests sur les fonctions du programme qui valident si les différents composants/fonctions d’une application fonctionnent toujours bien ensembles.
Nous allons écrire un test pour la fonction redis_visits_counter
qui implique un appel à la librairie redis
et à une véritable base Redis
(même si on pourrait facilement faire ici du mocking pour avoir un test unitaire plutôt)
integration_tests.py
dans tests
avec à l’intérieur:from ..monster_icon import redis_visits_counter
import redis
def test_redis_counter():
redis_cache = redis.StrictRedis(host='redis', port=6379, socket_connect_timeout=2, socket_timeout=2, db=0)
result = redis_visits_counter(redis_cache)
assert result
assert not isinstance(result, redis.RedisError)
assert isinstance(result, int)
result2 = redis_visits_counter(redis_cache)
assert result2 == result + 1
Expliquez ce que fait le test, en particulier l’initialisation du test et les assertions.
Lancez le test avec python -m pytest --verbose src/tests/integration_tests.py
que se passe-t-il ?
Lancez l’application complète avec docker-compose up -d --build
.
Relancez le test précédent. Pourquoi cela ne fonctionne-t-il toujours pas ?
Redis n’est accessible que dans le réseau docker. Nous devons donc lancez les tests depuis l’intérieur du conteneur monstericon et non pas depuis les sources à l’extérieur du conteneur.
Pour cela on peut utiliser docker-compose exec -it monstericon python -m pytest --verbose src/tests/unit_tests.py
L’intégration correcte de redis a été testée.
Les fonctionnels sont des tests pour vérifier le bon fonctionnement d’une application en train de tourner.
Nous disposons d’une application web basique. Une façon simple de vérifier son fonctionnement de l’extérieur est ainsi de lui envoyer des requêtes HTTP et de contrôler ses réponses.
functionnal_tests.py
dans tests
avec à l’intérieur le code suivant:"""Launch web functionnal test of monstericon application
Usage:
functionnal_tests.py <base_url>
"""
from docopt import docopt
import requests
if __name__ == '__main__':
arguments = docopt(__doc__)
response = requests.get(arguments['<base_url>'])
assert response.status_code == 200
assert 'Monster Icon' in response.text
response2 = requests.get(arguments['<base_url>']+'/monster/test')
assert response2.status_code == 200
print("Application seems Ok :)")
Expliquons le fonctionnement de ce script.
Lancez le script functionnal_tests.py
avec l’url http://localhost:5000
Vous pouvez récupérez la correction de ce TP en clonant comme suit : git clone -b jenkins_tp1_correction https://github.com/Uptime-Formation/corrections_tp.git jenkins_tp1_correction