Stratégie de test
Une application Archestack se teste par couches, de l'éditeur qui signale une erreur pendant la frappe au pipeline qui ne déploie pas tant qu'un test échoue. Les tests eux-mêmes font partie de l'application : ils vivent dans ses packages, l'accompagnent dans chaque espace de travail et s'exécutent sur une base de données sandbox qui leur est propre, donc un test ne lit ni ne modifie jamais les vraies données de qui que ce soit.
Cinq couches
| Couche | Ce qu'elle détecte | Quand elle s'exécute |
|---|---|---|
| L'éditeur | Du C# et du code de page qui ne compilent pas, une colonne ou un Business Entity qui n'existe pas, un Script Module appelé avec un mauvais argument | Pendant la frappe |
| Exécutions à blanc | Un script ou un Business Event qui ne fait pas ce qu'il faut, essayé avant d'être enregistré | Pendant l'écriture d'un script |
| Cas de test | Le comportement : la règle qu'un Business Event impose, ce qu'un Script Module retourne, ce qu'un Business Entity lit | À chaque modification, et avant chaque déploiement |
| Vérifications de schéma | Une modification de table qui perd des données, ou qui empêcherait un script typé de compiler | Avant chaque migration et chaque déploiement |
| Suites planifiées | Tout ce qui a changé autour de l'application : un autre package, une mise à niveau, des données qu'une règle n'attendait pas | Chaque nuit, ou selon n'importe quelle planification, dans chaque environnement |
Les deux premières ne demandent aucun travail : l'éditeur vérifie avec le même compilateur et le
même modèle de données que ceux avec lesquels l'application tourne, et le bouton
Test d'un Script Module ainsi que la simulation d'un Business Event s'exécutent à
blanc, c'est-à-dire que les écritures via Db et Files sont vérifiées et
enregistrées mais jamais faites, et que les mails et les notifications sont enregistrés mais pas
envoyés. Tout ce qu'un script fait d'autre par lui-même est réel : les appels HTTP,
System.IO, et tout ce que fait un
paquet NuGet, comme une connexion à une autre base de
données. La suite de cette page porte sur la
troisième couche, les tests que vous écrivez, et sur la façon dont les deux autres les exécutent.
Cas de test : given, when, then
Un cas de test est une affirmation sur l'application, en trois parties : les lignes à mettre en place (given), la seule chose à exécuter (when) et ce qui doit être vrai ensuite (then). Les cas sont regroupés en suites. Celui-ci, tiré de la démo Service Planner, prouve que le tableau de planification ne marque un jour comme surchargé que lorsque les heures planifiées d'un technicien dépassent sa capacité :
{
"name": "Week load flags a day over capacity",
"suite": "Service Planner",
"isEnabled": true,
"data": {
"description": "A day is overbooked only when its planned hours go over the technician's capacity.",
"given": [
{ "table": "service_technician", "rows": [
{ "name": "Ana Peeters", "daily_capacity_hours": 8 },
{ "name": "Bram Maes", "daily_capacity_hours": 8 }
] },
{ "table": "service_job", "rows": [
{ "title": "Heat pump install", "technician_id": 1, "planned_date": "2026-09-23", "estimated_hours": 5, "status": "Planned" },
{ "title": "Boiler service", "technician_id": 1, "planned_date": "2026-09-23", "estimated_hours": 4, "status": "Planned" },
{ "title": "Commissioning", "technician_id": 2, "planned_date": "2026-09-23", "estimated_hours": 8, "status": "Planned" }
] }
],
"when": { "kind": "ScriptModule", "name": "GetPlannerWeek", "parameters": { "week_start": "2026-09-21" } },
"then": [
{ "subject": "Result", "operator": "Succeeds" },
{ "subject": "Result", "operator": "Equals", "path": "technicians[0].days[2].overbooked", "expected": "true" },
{ "subject": "Result", "operator": "Equals", "path": "technicians[1].days[2].overbooked", "expected": "false" }
]
}
} Given
Les lignes dont le cas a besoin, table par table, écrites de haut en bas dans une sandbox vide. Les
lignes reçoivent les ids 1, 2, 3 dans l'ordre où elles sont écrites, et c'est vers eux que pointe la
clé étrangère d'une fixture suivante : ci-dessus, technician_id 1 est Ana. Une table
vers laquelle pointe une autre fixture passe donc en premier, et une clé que la base de données
génère est omise, tout comme les colonnes d'audit et toute colonne dont la valeur par défaut
convient. Dans le builder, choisir une table remplit des lignes d'exemple à partir des noms et des
types de ses colonnes, et une clé étrangère est un sélecteur parmi les lignes qu'écrivent les
fixtures placées au-dessus.
When
Exactement une cible, désignée par son nom, jamais par un id :
- Script Module : exécute un module avec les paramètres donnés.
- Business Entity : relit une entité, éventuellement filtrée, pour vérifier ce qu'une page afficherait.
- Business Event : déclenche un trigger sur une ligne, au moment indiqué (BeforeCreate, AfterUpdate, etc.), comme le ferait un enregistrement.
- Script : du C# libre avec tout ce dont dispose un Script Module, pour un cas qui doit faire plusieurs choses avant que l'affirmation puisse être vérifiée.
Then
Chaque vérification est un sujet, un opérateur et le plus souvent une valeur attendue. Les sujets
sont Result (ce que la cible a retourné), Rows (des lignes relues ensuite dans une
table), Output (ce que la cible a journalisé), Error, Duration et
Code. Les opérateurs sont Equals, NotEquals, Contains, NotContains, GreaterThan, LessThan,
IsNull, IsNotNull, Matches (une expression régulière), RowCount, Succeeds et Fails. Un chemin comme
technicians[0].days[2].overbooked descend dans un résultat, et un chemin qui ne mène
nulle part fait échouer la vérification et indique où il s'est arrêté.
Tester qu'une chose est refusée
Beaucoup des règles d'une application disent non : un Business Event refuse un enregistrement qui casserait quelque chose. Le cas qui teste une telle règle tente l'enregistrement et vérifie qu'il a été refusé, pour la bonne raison, et que rien n'a été écrit :
// A zero estimate is refused.csx: the target, a save the business event must stop
await Db.CreateAsync("service_job", new { title = "Check radiator valves", estimated_hours = 0 });
return "saved"; "then": [
{ "subject": "Result", "operator": "Fails" },
{ "subject": "Error", "operator": "Contains", "expected": "Estimated hours must be more than zero" },
{ "subject": "Rows", "operator": "RowCount", "table": "service_job", "expected": "0" }
] La deuxième vérification est celle qui compte. Fails seul réussit sur n'importe quelle erreur, une colonne mal orthographiée comprise, et c'est la façon la plus courante dont un test rouge cesse discrètement de tester quoi que ce soit. Rattachez l'échec à son message.
Vérifications de code
Une vérification qu'un seul sujet et un seul opérateur ne peuvent pas exprimer est une
vérification de code : du C# qui s'exécute après la cible dans la même sandbox,
avec tout ce dont dispose un Script Module, y compris les ensembles typés sur Db.
Result, Output, Error, DurationMs et
Failed contiennent le résultat de la cible, et Assert indique ce qu'il
attendait et ce qu'il a trouvé quand une vérification échoue (Equal, NotEqual, True, False, Null,
NotNull, Contains, Count et Fail). La vérification échoue quand elle lève une exception, fait
échouer un Assert ou retourne false.
// Planning a job marks it Planned.then.1.csx: a check one subject and one operator cannot say
var job = await Db.ServiceJob.SingleAsync(j => j.Title == "Boiler service");
Assert.Equal("Planned", job.Status);
Assert.NotNull(job.PlannedDate, "a planned job has a date");
var sameDay = await Db.ServiceJob
.Where(j => j.TechnicianId == job.TechnicianId && j.PlannedDate == job.PlannedDate)
.SumAsync(j => j.EstimatedHours);
Assert.True(sameDay <= 8, $"the day holds {sameDay} hours");
Utilisez-en une quand une vérification a besoin de plusieurs lignes à la fois, d'un calcul sur
celles-ci ou d'une comparaison entre deux tables. Dans un dépôt, chaque vérification de code est un
fichier à part, <Case>.then.<n>.csx, modifié avec les mêmes
fonctionnalités C# que n'importe quel script.
La sandbox
Un test ne touche jamais aux données de l'espace de travail. Une exécution redirige vers une sandbox la base de données qui contient les données, une base de données à part créée à la demande à l'image de la vraie, et chaque table qu'elle contient est vidée avant chaque cas, donc les seules lignes qu'un cas voit sont ses propres fixtures et ce que sa cible écrit. Les définitions viennent toujours de l'espace de travail, donc un test met à l'épreuve les Business Entities, les Business Events et les scripts qui sont sur le point d'être livrés. Chaque espace de travail a quatre sandboxes, donc quatre exécutions peuvent tourner en même temps ; une cinquième attend qu'une sandbox se libère.
- Les mails et les notifications sont enregistrés, pas envoyés. Le résultat de chaque cas liste ce que ses scripts auraient envoyé par mail et qui ils auraient notifié, donc un cas peut aussi le vérifier.
- Les fichiers restent tels quels. Les écritures, déplacements et suppressions via
Filesn'atteignent jamais le dossier de l'espace de travail : l'exécution les garde pour elle, si bien que relire un fichier qu'elle vient d'écrire fonctionne. - Les appels sortants sont réels. Tout ce qu'un script fait d'autre par lui-même se produit à chaque exécution : une requête HTTP, une connexion SMTP qu'il ouvre,
System.IO, et tout ce que fait un paquet NuGet, comme une connexion à une autre base de données, écritures comprises. Gardez les appels vers d'autres systèmes en dehors de ce que les tests exécutent, ou faites-les pointer vers un endpoint de test ou une base de données de test. - Le suivi est réel. L'exécution d'un cas enregistré consigne son résultat sur le cas et sur la suite et écrit une entrée dans l'Event Log. L'exécution d'un brouillon non enregistré, depuis le bouton Run de l'éditeur de cas ou depuis VS Code, ne consigne rien.
Où vivent les tests
Un test appartient au package qu'il teste. Il voyage avec ce package lors d'un export, d'un import
ou via le Module Store, donc un module arrive dans un nouvel endroit avec la preuve qu'il fonctionne
toujours, et tout ce qu'il désigne (le Script Module, le Business Entity, le Business Event), il le
désigne par son nom, donc il pointe vers la bonne chose dans n'importe quel espace de travail. Dans
un dépôt, les tests d'un package sont des fichiers : tests/<suite>/suite.json, un
fichier JSON par cas sous cases/, et un .csx à côté d'un cas pour une
cible Script ou une vérification de code. Ils sont relus et fusionnés comme le code qu'ils testent.
Exécuter les tests
- Dans le builder, la page Test Suite liste les suites et leurs cas, et exécute un cas, une suite ou tout, avec pour chaque vérification en échec l'attendu face au réel. Run, dans l'éditeur de cas, essaie le cas avant qu'il ne soit enregistré.
- Dans VS Code, Run Tests exécute un cas, une suite, un package ou tous les packages sur le projet, quatre cas à la fois, et affiche chaque résultat dans l'arborescence et dans la vue Testing. Voir Tests dans le guide VS Code.
- Selon une planification, une suite qui a un cron s'exécute d'elle-même, dans
l'environnement où elle est installée, quel qu'il soit :
0 0 2 * * ?signifie chaque nuit à deux heures. Chaque exécution écrit une entrée dans l'Event Log avec le détail de chaque vérification, et son dernier résultat figure sur la suite. - Dans le pipeline, les tests de chaque package s'exécutent sur un Archestack jetable construit à partir du dépôt avant que quoi que ce soit ne soit déployé, et un seul échec arrête le déploiement. Voir Stratégie de déploiement.
- Depuis un assistant IA, le MCP Server donne à un modèle connecté le même guide de test et les outils pour écrire un cas, l'exécuter et le corriger jusqu'à ce qu'il réussisse.
Lire un résultat
| Résultat | Signification |
|---|---|
| Passed | Chaque vérification a tenu. |
| Failed | La cible s'est exécutée et une vérification n'a pas tenu. Chaque vérification en échec indique ce qu'elle attendait et ce qu'elle a trouvé. |
| Error | Le cas n'est jamais arrivé à ses vérifications : une fixture n'a pas pu être insérée, une cible porte un mauvais nom, ou la cible a levé une exception alors que le cas ne s'y attendait pas. Lisez d'abord l'erreur. |
| Skipped | Le cas ou sa suite est désactivé. |
Quoi tester en premier
- Les Business Events qui protègent les données. Chaque règle qui refuse un enregistrement ou une suppression reçoit un cas qui est refusé et un cas qui est accepté, pour que la règle ne soit ni absente ni trop stricte.
- Les Script Modules qui calculent. À partir de quelques lignes, vérifiez les nombres que le module retourne, ainsi que les cas limites : un jour vide, un jour rempli exactement, un jour qui déborde.
- Les lectures dont l'application dépend. Une cible Business Entity avec un filtre, et un RowCount, prouvent qu'une page affiche les lignes qu'elle doit afficher et aucune de celles qu'elle ne doit pas afficher.
- Les parcours en plusieurs étapes. Une cible Script qui fait ce que ferait un utilisateur, et des vérifications de code sur tout ce qui aurait dû suivre.
Des tests qui restent utiles
- Un cas, une affirmation. Un cas qui vérifie dix choses ne dit rien sur celle des dix qui a cassé.
- Nommez le cas d'après la règle. "A zero estimate is refused" se lit comme une spécification dans la liste des résultats.
- Vérifiez le comportement, pas la fixture. Compter les lignes que vous venez d'insérer teste le chargeur de fixtures.
- Rattachez chaque échec à son message, comme plus haut.
- Gardez les fixtures petites. Uniquement les lignes dont l'affirmation a besoin, sans clés générées.
- Gardez les tests avec le code. Une modification et le test qui la prouve ont leur place dans le même package, et dans la même pull request.
Pour aller plus loin
Archestack dans VS Code explique comment exécuter les tests depuis l'éditeur, Stratégie de déploiement le pipeline qui les exécute avant chaque déploiement, et la référence les Business Events et les Script Modules sur lesquels portent la plupart des tests.