Partie 8: Mise en place des Tests Automatisés et du Linting
Introduction
Dans cette première phase de notre pipeline CI/CD, nous allons mettre en place les fondations essentielles : des tests automatisés et une vérification de la qualité du code. Cette phase établit les bases d'un développement rigoureux et professionnel.
Contexte et Objectifs
Situation Initiale
Le site home-fonta.fr est une application Flask simple avec 5 routes principales :
- Page d'accueil (
/) - Présentation (
/presentation) - Galerie (
/galerie) - Services (
/services) - Menu (
/menu) - Gestion des erreurs 404
Avant cette phase, le code était déployé manuellement sans aucune vérification automatique. Cela posait plusieurs risques :
- Possibilité d'introduire des bugs en production
- Pas de garantie que le code fonctionne après modification
- Style de code incohérent
- Pas de trace des tests effectués
Objectifs de la Phase 1
- Automatiser les tests : Vérifier automatiquement que toutes les routes fonctionnent
- Enforcer la qualité du code : Utiliser un linter pour respecter les standards Python (PEP 8)
- Intégration Continue : Exécuter ces vérifications automatiquement à chaque push
- Protection de la branche : Empêcher le merge de code défaillant
- Visibilité : Afficher le statut des tests via un badge
Architecture CI/CD Phase 1
┌─────────────────────────────────────────────────────────────────┐
│ Développeur │
│ │
│ 1. Modifie le code │
│ 2. git commit │
│ 3. git push │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Repository │
│ │
│ - Déclenche GitHub Actions workflow │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Actions (CI Workflow) │
│ │
│ Step 1: Checkout code │
│ Step 2: Setup Python 3.12 │
│ Step 3: Install dependencies │
│ Step 4: Lint with flake8 (vérification style PEP 8) │
│ Step 5: Run tests with pytest (6 tests) │
│ Step 6: Generate coverage report │
└────────────────────────┬────────────────────────────────────────┘
│
┌────────┴────────┐
│ │
▼ ▼
Tests PASS Tests FAIL
│ │
│ ▼
│ Merge bloqué
│ (branch protection)
│
▼
Merge autorisé
Implémentation Détaillée
Étape 1 : Structure du Projet
Avant de commencer, voici la structure du projet Flask :
home-fonta/
├── app.py # Application Flask principale
├── requirements.txt # Dépendances Python
├── static/ # Fichiers statiques (CSS, JS, images)
├── templates/ # Templates Jinja2 (HTML)
│ ├── index.html
│ ├── presentation.html
│ ├── galerie.html
│ ├── services.html
│ ├── menu.html
│ └── 404.html
├── tests/ # Suite de tests (nouveau)
│ ├── __init__.py
│ └── test_app.py
├── .github/ # Configuration GitHub
│ └── workflows/
│ └── ci.yml # Workflow CI (nouveau)
├── .gitignore # Fichiers à ignorer (nouveau)
└── README.md
Étape 2 : Création du .gitignore
Première chose à faire : créer un .gitignore pour éviter de committer des fichiers inutiles.
Fichier .gitignore :
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Virtual environments
venv/
env/
ENV/
# pytest
.pytest_cache/
.coverage
htmlcov/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
Ce fichier permet d'ignorer :
- Les fichiers compilés Python (
__pycache__/,*.pyc) - Les environnements virtuels
- Les fichiers de cache pytest
- Les fichiers spécifiques aux IDE et systèmes d'exploitation
Étape 3 : Création des Tests avec pytest
pytest est un framework de test Python moderne et puissant. Il permet d'écrire des tests de manière simple et élégante.
Fichier tests/__init__.py :
# Tests package
Ce fichier vide indique à Python que tests/ est un package.
Fichier tests/test_app.py :
import pytest
from app import app
@pytest.fixture
def client():
"""Create a test client for the Flask app"""
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_homepage(client):
"""Test that homepage returns 200"""
response = client.get('/')
assert response.status_code == 200
assert b'<!DOCTYPE html>' in response.data
def test_presentation_page(client):
"""Test that presentation page returns 200"""
response = client.get('/presentation')
assert response.status_code == 200
def test_galerie_page(client):
"""Test that galerie page returns 200"""
response = client.get('/galerie')
assert response.status_code == 200
def test_services_page(client):
"""Test that services page returns 200"""
response = client.get('/services')
assert response.status_code == 200
def test_menu_page(client):
"""Test that menu page returns 200"""
response = client.get('/menu')
assert response.status_code == 200
def test_404_page(client):
"""Test that 404 page is returned for unknown routes"""
response = client.get('/page-inexistante')
assert response.status_code == 404
Explication du code :
-
Fixture
client: Une fixture pytest est une fonction réutilisable qui prépare l'environnement de testapp.config['TESTING'] = True: Active le mode test de Flaskapp.test_client(): Crée un client de test Flask simulant un navigateuryield client: Fournit le client aux tests
-
Tests des routes : Chaque test vérifie qu'une route renvoie le code HTTP 200 (succès)
client.get('/route'): Simule une requête GETassert response.status_code == 200: Vérifie le code de retour
-
Test spécial homepage : En plus du code 200, on vérifie que la réponse contient du HTML valide (
<!DOCTYPE html>) -
Test 404 : Vérifie que les pages inexistantes renvoient bien une erreur 404
Exécution locale des tests :
# Installation des dépendances de test
pip install pytest pytest-cov
# Exécution des tests
pytest tests/ -v
# Avec couverture de code
pytest tests/ -v --cov=app --cov-report=term-missing
Résultat attendu :
tests/test_app.py::test_homepage PASSED [ 16%]
tests/test_app.py::test_presentation_page PASSED [ 33%]
tests/test_app.py::test_galerie_page PASSED [ 50%]
tests/test_app.py::test_services_page PASSED [ 66%]
tests/test_app.py::test_menu_page PASSED [ 83%]
tests/test_app.py::test_404_page PASSED [100%]
---------- coverage: platform linux, python 3.12.3 -----------
Name Stmts Miss Cover Missing
--------------------------------------
app.py 18 1 94% 37
--------------------------------------
TOTAL 18 1 94%
====== 6 passed in 0.05s ======
Étape 4 : Configuration de flake8
flake8 est un outil qui vérifie que le code Python respecte les conventions de style PEP 8.
Installation :
pip install flake8
Exécution :
# Vérification syntaxe critique (arrête le build si erreur)
flake8 app.py --count --select=E9,F63,F7,F82 --show-source --statistics
# Vérification style complète (max 120 caractères par ligne)
flake8 app.py --count --max-line-length=120 --statistics
Erreurs détectées initialement :
app.py:2:1: F401 'os' imported but unused
app.py:11:1: E302 expected 2 blank lines, found 1
app.py:15:1: E302 expected 2 blank lines, found 1
app.py:19:1: E302 expected 2 blank lines, found 1
app.py:23:1: E302 expected 2 blank lines, found 1
app.py:27:1: E302 expected 2 blank lines, found 1
app.py:31:1: E305 expected 2 blank lines after class or function definition, found 1
Corrections appliquées :
- Suppression de
import osnon utilisé (ligne 2) - Ajout de 2 lignes vides entre toutes les fonctions (PEP 8 standard)
Code app.py corrigé :
from flask import Flask, render_template
app = Flask(__name__, static_url_path='/static', static_folder='static', template_folder='templates')
@app.route('/')
def index():
return render_template('index.html')
@app.route('/presentation')
def presentation():
return render_template('presentation.html')
@app.route('/galerie')
def galerie():
return render_template('galerie.html')
@app.route('/services')
def services():
return render_template('services.html')
@app.route('/menu')
def menu():
return render_template('menu.html')
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)
Étape 5 : Création du Workflow GitHub Actions
GitHub Actions permet d'exécuter automatiquement des tâches (tests, déploiements, etc.) lors d'événements Git.
Fichier .github/workflows/ci.yml :
name: CI - Tests et Linting
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov flake8
- name: Lint with flake8
run: |
# Stop build if there are Python syntax errors or undefined names
flake8 app.py --count --select=E9,F63,F7,F82 --show-source --statistics
# Exit-zero treats all errors as warnings
flake8 app.py --count --max-line-length=120 --statistics
- name: Run tests with pytest
run: |
pytest tests/ -v --cov=app --cov-report=term-missing
- name: Test summary
if: always()
run: |
echo " Tests completed"
echo "Check the logs above for any failures"
Explication détaillée du workflow :
-
Déclencheurs (
on) :pushsur branchesmainoumasterpull_requestvers ces mêmes branches
-
Job
test:- S'exécute sur Ubuntu latest (machine virtuelle GitHub)
-
Étapes (
steps) :a. Checkout code : Clone le repository
uses: actions/checkout@v4b. Setup Python : Installe Python 3.12 avec cache pip
uses: actions/setup-python@v5 with: python-version: '3.12' cache: 'pip'Le cache pip accélère les builds en réutilisant les dépendances
c. Install dependencies : Installe Flask, pytest et flake8
run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install pytest pytest-cov flake8d. Lint with flake8 : Vérification en 2 passes
- Première passe : Erreurs critiques (syntaxe, noms non définis) → STOP le build
- Deuxième passe : Style PEP 8 complet
e. Run tests with pytest : Exécute les 6 tests avec rapport de couverture
pytest tests/ -v --cov=app --cov-report=term-missingf. Test summary : Affiche un résumé (s'exécute toujours, même si erreur)
if: always()
Étape 6 : Premier Échec du CI
Lors du premier push, le workflow a échoué avec les erreurs flake8 mentionnées précédemment.
Log d'erreur GitHub Actions :
Run flake8 app.py --count --select=E9,F63,F7,F82 --show-source --statistics
0
Run flake8 app.py --count --max-line-length=120 --statistics
app.py:2:1: F401 'os' imported but unused
app.py:11:1: E302 expected 2 blank lines, found 1
app.py:15:1: E302 expected 2 blank lines, found 1
app.py:19:1: E302 expected 2 blank lines, found 1
app.py:23:1: E302 expected 2 blank lines, found 1
app.py:27:1: E302 expected 2 blank lines, found 1
app.py:31:1: E305 expected 2 blank lines after class or function definition, found 1
7
Error: Process completed with exit code 1.
Résolution :
-
Correction du code localement
-
Tests locaux avant push :
python3 -m pytest tests/ -v # 6 passed flake8 app.py --max-line-length=120 # No errors -
Commit et push :
git add app.py git commit -m "Fix flake8 style errors: remove unused import, add blank lines" git push home-fonta master -
CI passé avec succès
Étape 7 : Badge CI et Protection de Branche
Ajout du badge dans README.md :
# 🏰 Home-Fonta.fr - Documentation complète

Le badge affiche en temps réel le statut du CI :
- 🟢 passing : Tous les tests passent
- 🔴 failing : Au moins un test échoue
Configuration de la protection de branche (GitHub) :
- Aller dans Settings → Branches → Branch protection rules
- Ajouter une règle pour
master - Cocher :
- Require status checks to pass before merging
- Require branches to be up to date before merging
- Sélectionner le job
testdans la liste
Effet : Impossible de merger une Pull Request si les tests ne passent pas.
Résultats et Bénéfices
Avant Phase 1
- Pas de tests automatisés
- Code style incohérent
- Risque de bugs en production
- Pas de vérification avant merge
- Processus manuel et chronophage
Après Phase 1
- 6 tests automatisés (couverture 94%)
- Code respecte PEP 8 (vérification automatique)
- CI exécuté automatiquement à chaque push
- Branche protégée : merge bloqué si tests échouent
- Badge visible affichant le statut
- Confiance accrue dans le code
Métriques
| Métrique | Valeur |
|---|---|
| Tests | 6/6 passing |
| Couverture | 94% |
| Lignes testées | 17/18 |
| Temps CI | ~30 secondes |
| Python version | 3.12 |
Commandes Utiles
Tests Locaux
# Exécuter les tests
pytest tests/ -v
# Avec couverture
pytest tests/ -v --cov=app --cov-report=term-missing
# Rapport HTML de couverture
pytest tests/ --cov=app --cov-report=html
open htmlcov/index.html
Linting Local
# Vérification complète
flake8 app.py --max-line-length=120
# Vérification seulement erreurs critiques
flake8 app.py --select=E9,F63,F7,F82
# Ignorer certaines règles
flake8 app.py --ignore=E501,W503
Git Workflow
# Modifier le code
vim app.py
# Tester localement
pytest tests/ -v
flake8 app.py --max-line-length=120
# Commit et push
git add app.py
git commit -m "Description des modifications"
git push origin master
# Le CI s'exécute automatiquement
# Vérifier sur GitHub Actions
Prochaines Étapes : Phase 2
La Phase 1 pose les fondations. Voici ce qui arrive dans la Phase 2 :
Phase 2 : Build et Push Docker Automatique
Objectifs :
- Construire automatiquement une image Docker à chaque push
- Tag avec le SHA du commit
- Push vers Docker Hub ou GitHub Container Registry
- Scan de sécurité de l'image (Trivy)
Workflow ajouté :
- name: Build Docker image
run: docker build -t home-fonta:${{ github.sha }} .
- name: Security scan
run: trivy image home-fonta:${{ github.sha }}
- name: Push to registry
run: docker push ghcr.io/nikob2o/home-fonta:${{ github.sha }}
Phases Futures
- Phase 3 : Mise à jour automatique du Helm chart avec nouvelle version
- Phase 4 : Environnements staging/production avec promotion automatique
- Phase 5 : Tests de sécurité, tests de charge, analyse SAST/DAST
- Phase 6 : Monitoring, alertes, rollback automatique
Conclusion
La Phase 1 du pipeline CI/CD est un succès. Nous avons mis en place :
- Suite de tests pytest complète (6 tests)
- Linting automatique avec flake8
- Workflow GitHub Actions fonctionnel
- Protection de branche active
- Badge CI visible
Ces fondations garantissent que chaque modification du code est testée et validée automatiquement. Le code ne peut plus être mergé s'il ne respecte pas les standards de qualité.
C'est la base d'un développement professionnel et rigoureux.
Dans le prochain article, nous verrons comment automatiser la création et le déploiement des images Docker.
Ressources :
- Documentation pytest
- Documentation flake8
- GitHub Actions Documentation
- PEP 8 Style Guide
- Flask Testing
Repository : github.com/Nikob2o/home-fonta