Les tests d’architecture logicielle : 4 exemples pour les adopter

Combien de fois, en tant que Tech Lead, avez-vous répété les mêmes remarques en code review ?
Combien de fois, architecte, vos recommandations sont-elles restées lettre morte ?
Combien de fois, développeur assisté par IA, avez-vous dû corriger un code généré qui ignorait vos consignes ?
Combien de fois une limite technique (ex un nom de variable trop long) vous a-t-elle surpris en production ?
Et combien de fois la documentation promise est-elle restée un simple ticket oublié ?

Si vous aussi, vous avez répondu “souvent” ou “trop souvent” à au moins l’une de ses questions, les tests d’architecture devraient vous plaire !

Le concept

Définition : Les tests d’architecture sont des tests visant à vérifier automatiquement des contraintes de conceptions, qu’elles concernent son organisation globale (couches, dépendances) ou des conventions (naming, patterns, règles d’usage d’API), indépendamment de son comportement fonctionnel.

Ils sont le pendant non-fonctionnel des tests automatisés “traditionnels” : unitaires / d’intégration / bout en bout.

Ils sont à intégrer dans la base de code au même titre que les autres tests et à lancer dans les hooks de pre-commit, la CI/CD. Ils doivent être bloquants.

Un exemple simple : Valider des noms

Contexte : une équipe qui développe un produit ayant une orchestration événementielle a un dispatcher qui se charge de récupérer tous les événements et les envoyer (dispatch) dans les bonnes queues.

Afin d’être robuste, l’équipe a décidé que son code créé les queues si elles n'existent pas encore.

queue_client = QueueClient(queue_name=queue_name, ...)
try:
  queue_client.send_message(message)
except ResourceNotFoundError:
  queue_client.create_queue()
  queue_client.send_message(message)

La contrainte : Sur azure, le nom d’une queue ne doit contenir que des lettres et des “-”.

  • ma-super-queue ✅
  • Ma_queue_mal_nommée2 ❌

Sans tests automatisés, il faut, en review s’assurer que la queue nouvellement ajoutée (ici dans l’inventaire des queues : Queues) respecte cette contrainte au risque d’avoir une erreur en production.

Le test :

def test_queue_names_format():
   pattern = re.compile(r"^[a-zA-Z-]+$")

   invalid_queues = []
   for queue in Queues:
       if not pattern.match(queue):
           invalid_queues.append(queue)

   if invalid_queues:
       pytest.fail(
           f"The following queue names contain invalid characters:\n"
           f"{sorted(invalid_queues)}"
       )

Ce test codé dans le dossiers tests/architecture sera lancé à chaque commit par le hook de pre-commit et dans la CI. Donnant ainsi la garantie de ne plus avoir de problèmes en production.

Ici la contrainte est celle donnée par le cloud provider, mais nous pouvons aisément imaginer une contrainte donnée par les standards de l’organisation, par exemple le nom des ressources doit être préfixé par le nom du projet.

Exemple 2 : Valider qu’une documentation existe

Contexte : Une équipe développant des flux de données pour une data plateforme doit également fournir des data contracts (voir les standards ODPS ou ODCS) : pour chaque data object un JSON qui décrit la donnée, son format, son schéma … Ces éléments sont ensuite consommés par le catalogue de donnée.

Le problème : Les datacontracts sont souvent oubliés d’être écrits au moment de tirer un flux.

Le test :

1. Récupérer l’ensemble des data objects, à partir du FileSystemTree, une classe qui rassemble l’ensemble des objets manipulés.

all_data_objects = [data_object for data_object in FileSystemTree]

2. Récupérer l’ensemble des data contracts disponibles, dans ce cas ce sont des .json dans le dossier /datalake_documentation

all_documentation = DATALAKE_DOC_PATH.rglob("documentation.json")

3. Valider qu’il n’y a pas de documentation manquantes, ni de documentation sans data objects (souvent lié à un problème de typo)

orphan_docs = set(all_documations) - set(all_data_objects)
doc_missing = set(all_data_objects) - set(all_documations)

Quelques remarques complémentaires :

  • A des fins de pédagogie, le code a ici été très simplifié pour illustrer le propos sans se préoccuper de la glue qui permet de comparer des documentations à des data objects.
  • Le fait que la documentation soit stockée dans le même repository que le code est clairement un accélérateur. Dans ce cas, elle est ensuite publiée par la CI/CD vers le catalogue de données. Cela est un des nombreux avantages du mono-repository.
  • Au moment de mettre en place le test, il existe sûrement de nombreuses documentations inexistantes, la stratégie est alors de tester que le nombre de documentations manquantes reste inchangé. Par exemple len(doc_missing) == 13. Par la suite, ce nombre sera amené à être abaissé.

Ainsi plus aucun développeur ne peut ajouter de data object sans au moins initier la documentation. Nous pouvons d’ailleurs ajouter des tests complémentaires pour valider que les champs obligatoires de la documentation sont remplis.

Exemple 3 : Valider le découpage de code et éviter le couplage

Contexte : Dans un projet qui suit un pattern d’architecture hexagonale, il s’agit de valider que le pattern choisi est correctement implémenté. Dans le cadre de ce projet il y a :

  • App <- Les interfaces : API / lambda
  • Domain
    • Port <- Des classes abstraites de contrat d’interface pour l’infrastructure
    • Entities <- Des classes métiers ou fonctions métiers
    • Usecase <- Des orchestrateurs (appelle la méthode lit, puis transforme, …)
  • Infrastructure <- Les implémentations des ports

La contrainte : Le pattern peut être difficile à prendre en main par un nouvel arrivant dans l’équipe, ou par un agent de code.

Le test :

1. Configurer les imports tolérés

ALLOWED_IMPORTS = {
   "domain/entities/": ["config", "domain.entities"],
   "domain/ports/": ["config", "domain.entities"],
   "domain/usecases/": ["domain.entities", "domain.ports", "config", "domain.usecases"],
   "infrastructure/": ["domain", "config"],
   "config/": ["config"],
   "app/": ["config", "domain.usecases", "infrastructure"]
}

2. Récupérer l’ensemble des imports par l’ensemble des scripts.

Ici nous vous passons le code un peu volumineux à base de glob, if, grep, etc. Un assistant de code sera très à l’aise pour le générer à votre place.

3. Procéder aux validations

def test_import_rules():
   for file_path in find_python_files(domain_folder):
       imports = parse_imports(file_path)
       is_valid, key, imported = check_import_rules(file_path, imports)
           assert is_valid, (
               f"File {file_path} in {key} imports {imported}, "
               f"which is not allowed.")

Ce test permet ainsi au développeur d’avoir une feedback loop rapide sur le bon respect du pattern d’architecture, d’éviter l’érosion architecturale, de garantir l’indépendance du domaine métier au intéraction avec le cloud provider et les utilisateurs.

Exemple 4 : S’assurer que des méthodes critiques sont appelées avec les bon arguments

Cet exemple est peut-être un peu plus de niche, mais permet d’illustrer la puissance de ces tests.

Contexte : Ayant un ensemble de micro-services écrivant des fichiers parquet parfois partitionnés, il convient d’avoir une stratégie claire si la partition existe déjà, est-ce un append ou un replace.

Le fichier partitionné peut ressembler à

fichier.parquet/
├── date=2026-01-02/
│   └── part-0000.parquet
├── date=2026-01-03/
│   └── part-0000.parquet


La nouvelle écriture d’une partition sur 2026-01-03 peut signifier l’ajout de nouvelles données (un append), ou le remplacement de la donnée pour cause de rejeu d’un script. Il convient de la choisir consciemment au moment de l’écriture au risque de supprimer des données ou de les dupliquer.

En python le code attendu est :

df.to_parquet(path, partition_cols=["date"],
                   existing_data_behavior='overwrite_or_ignore', ...)

Ayant rencontré une fois le cas de duplication de données pour cause d’argument existing_data_behavior manquant, l’équipe a implémenté un test pour vérifier cela.

La contrainte : s’assurer que la méthode est appelé avec l’argument existing_data_behavior

Le test :

Si il n’est pas très important de comprendre complètement l’implémentation, l’idée est de voir que le test se décompose en deux :

1. Construire un visiteur qui est capable d’explorer l’Abstract Syntaxe Tree qui représente la façon dont les méthodes s’appellent entre elles.

class ToParquetCallVisitor(ast.NodeVisitor):
   def __init__(self):
       self.errors = []
   def visit_Call(self, node):
       if isinstance(node.func, ast.Attribute) and node.func.attr == 'to_parquet':
           partition_cols_present = any(kw.arg == 'partition_cols' for kw in node.keywords)
           existing_data_behavior_present = any(kw.arg == 'existing_data_behavior' for kw in node.keywords)
           if partition_cols_present and not existing_data_behavior_present:
               self.errors.append(f"File: {self.current_file}, Line: {node.lineno}")
       self.generic_visit(node)

Le code explore récursivement toutes les méthodes, si il arrive sur une méthode appelée to_parquet appelé avec l’argument partition_cols qui signifie que le fichier sera partitionné, alors il vérifie qu’il y a bien également l’argument existing_data_behavior.

2. Lancer le test sur tous les fichiers

   visitor = ToParquetCallVisitor()
   for root, _, files in os.walk(SOURCES_PATH):
       for file in files:
           if file.endswith('.py'):
               file_path = os.path.join(root, file)
               with open(file_path, encoding='utf-8') as f:
                   file_content = f.read()
               tree = ast.parse(file_content, filename=file_path)
               visitor.current_file = file_path
               visitor.visit(tree)
   if visitor.errors:
       error_messages = "\n".join(visitor.errors)
       pytest.fail(f"Missing 'existing_data_behavior' in 'to_parquet' calls:\n{error_messages}")

Ici le code explore l’ensemble des fichier de code source pour faire appel au visiteur vu en partie 1, puis fail en cas d’erreur identifiées.

NB : Écrire un test capable d’explorer l’AST peut sembler complexe, cependant un assistant de code est lui capable de faire cela sans soucis.

L’impact des agents de code

Même si ces tests ont tout leur sens sans agent de code, au moment d’écrire cet article il est impossible de ne pas les aborder plus largement.

Les agents de code se sont très rapidement répandus ces 2 dernières années, en novembre 2025 ils représentaient 15% des Pull Request sur GitHub. Ils sont un accélérateur indéniable pour la production de code, cependant la maîtrise de ceux-ci est encore un sujet d’exploration : la construction d’AGENTS.md, ou de mise d’un harnais comme le propose OpenAI.

Le test d’architecture comme un moyen de maîtriser son agent

L’ensemble des tests d’architecture sont un très bon harnais de sécurité, plus robuste qu’un prompt complexe, pour s’assurer que l’agent de code produit du code qui respecte les standards.

Dans le prompt ou le skill fourni, il suffit alors de lui dire de lancer les tests d’architecture afin de vérifier de manière déterministe la validité d’un pattern.

Les explorations d’OpenAI citées précédemment montrent que le design d’architecture doit être fait au plus tôt, et donc ces tests pourraient être écrits dès le début du projet.

Vigilance tout de même à ne pas paralyser le développement en mettant en place des tests trop restrictifs.

L’agent de code comme accélérateur

De manière complémentaire, l’ayant discuté dans les exemples précédents il est tout à fait possible d’utiliser un agent de code pour implémenter ces tests.

L’équipe a l’habitude d’utiliser copilot intégré au sein de l’IDE en mode “agent”, signifiant qu’il peut explorer la code base pour lui permettre d’apporter des réponses pertinentes.

La démarche est alors la suivante :

1. Exprimer clairement ce que l’on souhaite valider

Peux-tu m'écrire un test qui valide que l'ensemble queue names dans la class Queues ont bien que des lettres en minuscule et des "-".

NB: Le prompt est très simple, mais est satisfaisant dans le contexte de cette équipe, car elle lui met à disposition un AGENTS.md qui lui donne toutes les informations nécessaires, et il peut s’inspirer des tests déjà existants pour comprendre la logique.

2. Vérifier que le test est bien rouge lorsque l’on fait exprès d’implémenter une erreur
3. Vérifier que le test est bien vert lorsque toutes les erreurs sont corrigées
4. Probablement refactorer un petit peu le test après pour l'alléger et vérifier qu’il a bien validé le comportement souhaité.

Il convient cependant de s’assurer qu’il n’existe pas déjà un outil pour valider le comportement souhaité :

  • Par exemple, un agent de code n’aura aucun problème à coder un test complexe qui valide que la longueur maximale des lignes est bien de 120 caractères, cependant il convient d’utiliser un linter pour ce genre de vérifications.
  • En écrivant cet article, les relecteurs a d’ailleurs fait remarquer qu’il existe des linter pour valider les pattern d’architecture, plutôt qu’utiliser le test de l’exemple 3 (import linter en python, ou ts-arch en js)

Un retour d’expérience pour finir

Dans une équipe de 4 data engineers de moins de 3 ans d’expérience professionnelle et un architecte de 10 ans d’expérience, développant et maintenant plus de 200 microservices en production nous avons mis en place une vingtaine de tests d’architecture.

Ces tests apportent plusieurs avantages :

  1. Réduction drastique du nombre de bug en production, le code déployé respecte l’ensemble des contraintes, que l’on connaît à date, sur notre infrastructure
  2. Transformation des code reviews en moment d’alignement entre les membres de l’équipe: au moins deux personnes ont vu ce code. Plutôt que de rappeler des standards d’équipe. Les code reviews sont donc beaucoup plus rapides et moins douloureuses
  3. Une documentation des data contracts presque exhaustive.

La chose la plus marquante est peut-être le verbatim (paraphrasé) suivant :

Les tests d’architecture, c’est un peu toi (architecte) qui est à côté de moi en train de me dire Nope ! … Encore Nope !

Voici une liste exhaustive des tests actuellement implémentés dans ce projet :

  • 📋 Configuration & Déploiement
    • Cohérence des Queue Names - Vérifie que les noms de queues
    • Utilisation des toutes les queues - S'assure que toutes les queues définies dans l'enum Queues sont utilisées dans au moins un microservice.
    • Déploiement de tous les microservices - Contrôle que tous les dossiers azfn_* du code source sont présents dans le pipeline de déploiement.
    • Longueur des noms de fichiers - S'assure que les noms de fichiers ne dépassent pas 100 caractères (compatibilité Windows)
    • Cohérence des versions - Vérifie que les dépendances communes entre les microservices ont les mêmes numéros de version.
  • 🎯 Triggers & Schedules (Cron)
    • Validité des Cron- Valide la syntaxe NCRONTAB de tous les schedules cron
    • Timer Triggers non simultané - Vérifie qu'aucune fonction n'a de time triggers qui s'exécutent au même moment pour un souci spécifique lié à notre infrastructure.
    • Définition d’une politique de retry - Vérifie que toutes les azure functions ont une retry policy configurée.
    • Délai de retry pour l’Ingestion - Validate que les fonctions d'ingestion time trigger ont 1 delai de retry ≥ 1min, pour espacer les retry sur une API éventuellement indisponible.
  • 🏗️ Architecture & Imports
    • Utilisation de méthode de connexion au datalake custom, outillé plutôt que la méthode standard azure.
    • Architecture hexagonale (l’exemple 3)
    • Indépendance d’un code qui doit être déployé on-premise - Contrôle que le code dans le dossier jobs_onprem/ n'importe rien en dehors de son scope
  • 💾 Données
    • Gold File Name Format - Vérifie que les noms de fichiers utilisent uniquement des underscores et pas de tirets.
    • Stratégie de partitionnement - S'assure que quand partition_cols est utilisé dans to_parquet(), existing_data_behavior est aussi défini
    • Existence de la documentation - Contrôle que tous les DataObjects du FileSystemTree ont leur documentation.json correspondante dans datalake_documentation/
    • Format de la documentation - Valide que tous les fichiers documentation.json sont du JSON valide
  • 🔄 Concurrence & Leases
    • Function non concurrentes - Vérifie que les fonctions ne supportant pas la concurrences non-concurrentes sont bien déployés sur une ressource assurant la non-concurrence;
    • Utilisation de lease que dans les ressources non concurrentes - S'assure que les usecases utilisant acquire_lease_on_data_object sont appelés uniquement par des fonctions sans concurrence
    • Mise en place d’un lease - Contrôle que les opérations d'écriture sur les gold files utilisent les leases pour éviter les concurrences en cas de concaténation de données.

Conclusion

Les tests d'architecture sont un outil puissant pour automatiser la vérification des patterns d’architecture, des standards, de valider les choses qu’un linter ne peut pas aisément valider.

A l’heure des agents de code, il est facile de les écrire et de cadrer l’agent comme les développeurs nouvellement arrivés sur le projet.

Remerciements : Baptiste O'jeanson, Loïc LEFLOCH, Jean-Baptiste Larraufie, Olivier Acar pour leurs relectures.