Partie 8: Mise en place des Tests Automatisés et du Linting

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

  1. Automatiser les tests : Vérifier automatiquement que toutes les routes fonctionnent
  2. Enforcer la qualité du code : Utiliser un linter pour respecter les standards Python (PEP 8)
  3. Intégration Continue : Exécuter ces vérifications automatiquement à chaque push
  4. Protection de la branche : Empêcher le merge de code défaillant
  5. 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 :

  1. Fixture client : Une fixture pytest est une fonction réutilisable qui prépare l'environnement de test

    • app.config['TESTING'] = True : Active le mode test de Flask
    • app.test_client() : Crée un client de test Flask simulant un navigateur
    • yield client : Fournit le client aux tests
  2. Tests des routes : Chaque test vérifie qu'une route renvoie le code HTTP 200 (succès)

    • client.get('/route') : Simule une requête GET
    • assert response.status_code == 200 : Vérifie le code de retour
  3. Test spécial homepage : En plus du code 200, on vérifie que la réponse contient du HTML valide (<!DOCTYPE html>)

  4. 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 :

  1. Suppression de import os non utilisé (ligne 2)
  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 :

  1. Déclencheurs (on) :

    • push sur branches main ou master
    • pull_request vers ces mêmes branches
  2. Job test :

    • S'exécute sur Ubuntu latest (machine virtuelle GitHub)
  3. Étapes (steps) :

    a. Checkout code : Clone le repository

    uses: actions/checkout@v4
    

    b. 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 flake8
    

    d. 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-missing
    

    f. 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 :

  1. Correction du code localement

  2. Tests locaux avant push :

    python3 -m pytest tests/ -v        # 6 passed
    flake8 app.py --max-line-length=120 # No errors
    
  3. 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
    
  4. 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

![CI Status](https://github.com/Nikob2o/home-fonta/actions/workflows/ci.yml/badge.svg)

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) :

  1. Aller dans SettingsBranchesBranch protection rules
  2. Ajouter une règle pour master
  3. Cocher :
    • Require status checks to pass before merging
    • Require branches to be up to date before merging
    • Sélectionner le job test dans 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 :

  1. Construire automatiquement une image Docker à chaque push
  2. Tag avec le SHA du commit
  3. Push vers Docker Hub ou GitHub Container Registry
  4. 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 :

  1. Suite de tests pytest complète (6 tests)
  2. Linting automatique avec flake8
  3. Workflow GitHub Actions fonctionnel
  4. Protection de branche active
  5. 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 :

Repository : github.com/Nikob2o/home-fonta

Read more