Je suis arrivé sur S37 Portal sans aucun test en place. Pas de convention, pas de CI, rien. Le produit tournait, des clients l'utilisaient, et ma mission c'était de construire tout ça de zéro.
Le produit
S37 Studio est une agence digitale AI-native. Le problème de toutes les agences, c'est que le scaling est limité par le temps des personnes qui travaillent là. S37 a construit S37 Portal pour changer ça : une plateforme qui centralise toutes les données de l'agence et fait tourner des agents IA sur les missions.
J'ai découvert un produit qui remplace un tas d'outils dispersés (CRM, gestion de projet, docs clients, facturation) par une seule base. Companies, contacts, deals, missions, tâches, documents, factures : tout au même endroit. Les clients ont accès à leur espace pour suivre leurs missions et leurs livrables.
S37 a travaillé avec des boîtes comme Swan, HappyPal ou Flex AI. Un bug sur la gestion des proposals ou des missions, c'est pas anodin.
Ce qui m'a frappé par rapport à un CRM classique, c'est la couche agents. Un agent reçoit tout le contexte d'un coup (historique de la company, tâches en cours, documents, outils disponibles), fait le travail, et l'équipe valide. L'équipe pilote. Les agents exécutent.
Lire la spec avant de coder
Avant d'écrire quoi que ce soit, j'ai lu la spec OpenAPI de bout en bout. Modèle de données, relations entre ressources, codes de statut attendus, règles de validation. C'est là que j'ai vu où les risques étaient et ce qui méritait d'être couvert en priorité.
J'ai aussi construit des scénarios de bout en bout pour comprendre comment le produit était vraiment utilisé : un lead contacte l'agence, une company est créée, un deal est ouvert, une proposal est envoyée, le client l'accepte, une mission démarre. J'ai cartographié ces enchaînements pour identifier les ressources critiques et les endroits où un bug aurait le plus d'impact sur le business.
Un repo séparé
J'ai créé s37-tests comme repo indépendant plutôt que d'intégrer les tests dans le repo principal de Portal. La raison : Portal est en TypeScript. Mélanger Pytest dans la même codebase aurait compliqué la gestion des dépendances et de la CI sans rien apporter.
J'ai aussi forcé une discipline utile en séparant les repos : les tests ne passent que par l'API publique, pas par le code source de l'application. Je teste ce que l'API expose, pas comment elle est construite en interne. C'est une contrainte qui protège contre les mauvaises habitudes à long terme.
Structure de la codebase
J'ai organisé le repo en un seul dossier tests/api/, un fichier par ressource. Toutes les fixtures partagées (auth, données, teardown) sont dans tests/conftest.py :
J'ai découpé par ressource parce que quand un test échoue, je sais immédiatement où regarder. Quand une nouvelle ressource est ajoutée à l'API, j'ajoute un fichier. La structure suit le produit.
J'ai commencé par portal_url, qui lit l'URL depuis les variables d'environnement et plante proprement si elle est absente :
J'ai ensuite créé admin_client pour ouvrir une session HTTP avec requests.Session() et garder le cookie d'auth entre les requêtes. Sans ça, je devais me reconnecter à chaque test, et les premiers runs prenaient trois fois plus longtemps :
J'ai aussi écrit des fixtures de données qui créent la ressource avant le test et la suppriment après via yield, même si le test plante. J'ai découvert ça à la dure : mes premiers tests laissaient des données orphelines en base à chaque run raté. Avec yield, le nettoyage se fait quoi qu'il arrive :
Couverture API
Sur chaque ressource (Companies, Contacts, Deals, Proposals, Missions, Mission Tasks), je couvre quatre cas : le happy path, les erreurs de validation, les requêtes non authentifiées, et les règles métier. Un test, un comportement :
Documenter les anomalies
En écrivant les tests, j'ai trouvé des endpoints où l'API se comportait différemment de ce que la spec décrivait. La ressource était bien créée, mais le statut retourné ne correspondait pas au contrat OpenAPI. Fonctionnellement ça marchait. Le contrat, non.
Plutôt que d'ignorer ces cas, j'ai utilisé @pytest.mark.xfail(strict=True) pour les tracer. Je garde le test actif dans la CI, il est censé échouer pour l'instant. Si l'API est corrigée, il passe en XPASS et je reçois une alerte :
J'ai remonté chaque anomalie au CTO avec le contexte et la référence à la spec. Au total, on a identifié plusieurs comportements non conformes avant qu'ils ne causent de vrais problèmes en production. Tout est tracé dans GitHub Issues.
Pipeline CI/CD
J'ai branché GitHub Actions sur chaque PR. La CI vérifie le formatage avec Black, le linting avec Flake8, et s'assure que tous les tests se collectent sans erreur de syntaxe. Les tests eux-mêmes tournent en local ou sur l'environnement de staging. La CI garantit que le repo reste propre à chaque merge :
J'ai montré ça au CTO. Un coup de `git push`, et la PR montre immédiatement si le code est formaté et si les tests peuvent être collectés. Il l'a intégré dans son process de review dès le premier jour.
AI-Driven development
J'ai construit un Cursor Skill generate-api-tests pour accélérer la génération de tests à partir de la spec OpenAPI. Je lui donne un endpoint, il génère la structure de tests en respectant les conventions du framework :
Sur une vingtaine d'endpoints, je génère les tests de base en quelques secondes. Je garde le temps restant pour les cas qui demandent du jugement : les règles métier, les interactions entre ressources, les cas limites que la spec ne documente pas.
Bilan
J'ai livré les tests API sur les six ressources principales (companies, contacts, deals, proposals, missions, mission tasks) et le pipeline CI. J'ai aussi remonté plusieurs comportements non conformes à la spec que personne n'avait repérés avant. Le CTO a intégré les tests dans son process de review dès les premiers PR.
Sur ce projet, j'ai passé plus de temps à lire et comprendre le système qu'à écrire des tests. J'ai cartographié des dizaines de scénarios métier avant de toucher à une seule ligne de code. C'est ce travail en amont qui a rendu chaque test utile plutôt qu'anecdotique.