@mostajs/mjs-unit
v0.10.0
Published
Micro-framework de test pour l'écosystème @mostajs/* : zéro dépendance, API minimale (test/expect/run), runner CLI (preuves HTML/JSON/JUnit XML via --report/--json/--junit ; compte rendu de validation CRA lié à la SBOM, CTRF, in-toto test-result via --com
Maintainers
Readme
@mostajs/mjs-unit
Auteur : Dr Hamid MADANI [email protected] · Licence : AGPL-3.0-or-later
Micro-framework de test zéro dépendance pour l'écosystème @mostajs/*. Trois apports :
- API minimale :
test(),describe(),expect()+ assertions (ok,equal,deepEqual,throwsAsync,doesNotThrowAsync). - Runner CLI (
mjs-unit <fichiers>) : importe les fichiers de test, exécute, affiche un rapport, code de sortie ≠ 0 si échec. - Resolver ESM : exécute sous Node les
dist/à imports sans extension (conventionmoduleResolution: bundlerdes modules @mostajs) — sans ce resolver,nodeéchoue surimport './x'.
Installation
npm i -D @mostajs/mjs-unit
# module local non publié : npm i -D "@mostajs/mjs-unit@file:../chemin/mosta-mjs-unit"How to use (pas à pas, dans n'importe quelle application)
- Installer le module en devDependency (voir ci-dessus).
- Créer un dossier
test-scripts/et y écrire des fichiers*.test.mjs(un test =test(name, fn); ne pas appelerrun(), le runner s'en charge). - Versionner un runner
test-scripts/run-tests.shqui (re)bâtit puis lance le CLI. - Brancher
package.json:"scripts": { "test": "bash test-scripts/run-tests.sh" }. - Lancer :
npm test(code de sortie ≠ 0 si un test échoue → utilisable en CI).
// test-scripts/api.test.mjs — test d'unité + test web, dans la même suite
import { test, equal, ok } from "@mostajs/mjs-unit";
import { createHttpTester, waitForEvent } from "@mostajs/mjs-unit/web";
import { app } from "../dist/app.js"; // votre app Express / handler natif
const api = createHttpTester(app);
test("GET /healthz → 200", async () => {
const res = await api.get("/healthz");
equal(res.status, 200);
ok(res.body.status === "ok");
});# test-scripts/run-tests.sh
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
npm run build >/dev/null
node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjsBesoin d'un profil d'environnement (DB de test, secrets…) avant d'importer le
dist/? Fixez-le avant les imports via un import dynamique :process.env.APP_ENV = "TEST"; const { app } = await import("../dist/app.js");(lesimportstatiques sont hoistés, donc exécutés avant le code du module).
Écrire un test (test-scripts/mon.test.mjs)
import { test, expect, ok } from "@mostajs/mjs-unit";
import { maFonction } from "../dist/lib/ma-fonction.js"; // le dist peut avoir des imports sans extension
test("addition", () => expect(maFonction(2, 2)).toBe(4));
test("async", async () => { ok(await maFonction.async()); });Ne pas appeler
run()dans le fichier : le runner s'en charge (les cas enregistrés viatest()sont exécutés groupés par fichier).Depuis 0.7.0, le runner relève ce qu'il a exécuté : par fichier, cas enregistrés contre cas exécutés, et il imprime le compte (
✓ 49/49 test(s) exécutés sur 7 fichier(s)). Un fichier sans aucun test fait échouer la suite ; un fichier qui appellerun()lui-même reste au rapport ; etnode fichier.test.mjsdont les cas ne sont jamais exécutés sort en code 1, avec un message — l'absence de résultat n'est pas un succès.
Lancer
mjs-unit test-scripts/*.test.mjs # via le bin
# ou, programmatique :
node --import @mostajs/mjs-unit/register test-scripts/mon.test.mjs # + appeler run() soi-mêmeRunner versionné conseillé (test-scripts/run-tests.sh) :
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
npm run build >/dev/null
node node_modules/@mostajs/mjs-unit/bin/cli.mjs test-scripts/*.test.mjsRapport JUnit XML (0.8.0)
mjs-unit test-scripts/*.test.mjs --junit=test-scripts/.out/junit.xml --timeout=10000Le fichier suit le schéma lu par Jenkins, GitLab (artifacts:reports:junit), GitHub et QATrax : une
<testsuite> par fichier, un <testcase name classname file time> par cas, <failure> pour une assertion
tombée, <error> pour une exception inattendue ou un délai dépassé, <skipped message> pour un cas ignoré,
les étiquettes en <property name="tag">. --tag=securite ne joue que les cas étiquetés.
Compte rendu de validation — cycle CRA (0.9.0)
npx mjs-sbom # la SBOM de la version publiée (@mostajs/mjs-sbom)
npx mjs-unit test-scripts/**/*.test.mjs \
--sbom=sbom.cyclonedx.json --plan=docs/DEVTEST-PLAN.<projet>.json \
--compte-rendu --ctrf --intoto
# 📄 Compte rendu : test-scripts/.out/compte-rendu.json — @acme/[email protected] — verdict : conforme
npx mjs-unit-menu # ou le menu : essais, SBOM, compte rendu, envoi à qatrax.devLe compte rendu lie les résultats à la version (lue dans la SBOM), au plan et aux scripts (empreintes) ; son verdict
est conforme, non conforme ou incomplet (avec les défauts) ; il s'écrit toujours. qatrax.dev l'enregistre, le
relie à l'analyse des vulnérabilités de la même SBOM, et scelle le dossier.
Java, C#, Python, Go, C++… — le mode lecteur (0.10.0)
Vos essais restent joués par l'outil de votre langage ; mjs-unit lit leur rapport et produit le même compte rendu scellé (lié à la SBOM et au plan), avec CTRF et in-toto. Node ≥ 18 suffit dans la CI ; aucun JavaScript à écrire.
Formats lus tels quels, reconnus à leur contenu : JUnit XML · NUnit 2 et 3 · TRX (VSTest) · TestNG natif ·
Robot Framework (output.xml) · CTest (Test.xml) · Cucumber JSON · Behave JSON · Allure 2 (allure-results) ·
CTRF · TAP · JSON mjs-unit. Votre rapport natif suffit : aucune conversion.
| Langage | Rapport | Commande |
|---|---|---|
| Java (Maven) | target/surefire-reports/TEST-*.xml | npx @mostajs/mjs-unit --depuis=target/surefire-reports --sbom=target/bom.json --plan=plan.json --compte-rendu |
| Java (Gradle) | build/test-results/test/TEST-*.xml | npx @mostajs/mjs-unit --depuis=build/test-results/test … |
| C# (.NET) | dotnet test --logger "junit;LogFilePath=resultats.xml" | npx @mostajs/mjs-unit --depuis=resultats.xml … |
| Python | pytest --junitxml=resultats.xml | npx @mostajs/mjs-unit --depuis=resultats.xml … |
| Go | go test -v ./... 2>&1 \| go-junit-report > resultats.xml | npx @mostajs/mjs-unit --depuis=resultats.xml … |
| C# (NUnit) | TestResult.xml (nunit3-console) | npx @mostajs/mjs-unit --depuis=TestResult.xml … |
| C# (TRX) | dotnet test --logger trx → TestResults/*.trx | npx @mostajs/mjs-unit --depuis=TestResults … |
| Java (TestNG) | target/surefire-reports/testng-results.xml | npx @mostajs/mjs-unit --depuis=target/surefire-reports/testng-results.xml … |
| Robot Framework | output.xml | npx @mostajs/mjs-unit --depuis=output.xml … |
| C/C++ (CTest) | ctest -T Test → Testing/<date>/Test.xml | npx @mostajs/mjs-unit --depuis Testing/*/Test.xml … |
| BDD (Cucumber) | --format json:cucumber.json (JS, JVM, Ruby) | npx @mostajs/mjs-unit --depuis=cucumber.json … |
| BDD (Behave) | behave -f json -o behave.json | npx @mostajs/mjs-unit --depuis=behave.json … |
| Allure (tout langage) | allure-results/ | npx @mostajs/mjs-unit --depuis=allure-results … (conteneurs et pièces jointes écartés ; un essai rejoué : le dernier fait foi) |
Nommez vos essais d'après le plan : test_T_MOD_01_… est rattaché à T-MOD-01. En BDD, Robot ou Allure, une
étiquette égale à la référence suffit (@T-MOD-01). Avec le menu :
MJS_DEPUIS=target/surefire-reports npx mjs-unit-menu --choix=3, puis --choix=4 (plan, SBOM, compte rendu vers
qatrax.dev). Les lecteurs sont exportés : import { lireRapportEssais, refDuPlan } from "@mostajs/mjs-unit/lecteurs".
Ce que le sceau prouve : le rapport n'a pas changé après sa lecture, avec cette SBOM et ce plan. Il ne prouve pas que l'outil du langage a joué les essais honnêtement.
API
| Fonction | Rôle |
|---|---|
| test(name, fn, { timeout?, tags? }) | Enregistre un cas (sync ou async) ; délai et étiquettes à la JUnit |
| test.skip(name, fn?, raison?) | Cas ignoré, raison rapportée (@Disabled) |
| test.each(table)(name, fn) | Un cas par ligne ; %s, %i, $cle dans le nom (@ParameterizedTest) |
| describe(group, body) | Groupe : préfixe les noms, et devient le classname JUnit |
| beforeAll/afterAll/beforeEach/afterEach(fn) | Crochets, au niveau du fichier ou d'un describe |
| expect(v).toBe/toEqual/toBeTruthy/toBeFalsy/toContain | Assertions fluides |
| ok/equal/deepEqual(...) | Assertions directes |
| throwsAsync/doesNotThrowAsync(fn) | Vérifie qu'une promesse (ne) lève (pas) |
| run(label?) | Exécute les cas en attente → {passed, failed, skipped} (appelé par le CLI) |
Zéro dépendance runtime ; @types/node en dev uniquement.
Tests web (@mostajs/mjs-unit/web)
Moteur générique pour tester des applications HTTP et événementielles — sans
supertest, jest ni client socket dans le framework. Toujours zéro
dépendance (uniquement node:http/node:net).
| Fonction | Rôle |
|---|---|
| startServer(target) | Démarre un serveur HTTP éphémère (port 0) autour d'un http.Server, d'un RequestListener ou d'une app Express → { url, port, close() } |
| createHttpTester(target) | Client HTTP façon supertest : get/post/put/del/request → { status, headers, body }. target = URL d'un serveur démarré ou handler/serveur (éphémère par requête) |
| waitForEvent(emitter, name, { timeout }) | Attend un événement sur tout émetteur (EventEmitter, client Socket.io, ws…) — teste le temps réel |
import { test, equal, ok } from "@mostajs/mjs-unit";
import { createHttpTester, startServer, waitForEvent } from "@mostajs/mjs-unit/web";
// HTTP : éphémère par requête (app Express ou handler natif (req,res)=>…)
const api = createHttpTester(app);
test("login", async () => {
const res = await api.post("/login", { username: "admin", password: "admin" });
equal(res.status, 200);
ok(res.body.token);
});
// Temps réel : serveur durable + attente d'événement (ex. Socket.io)
test("push", async () => {
const srv = await startServer(httpServer); // serveur partagé
const client = ioClient(srv.url); // socket.io-client fourni par l'app
const received = waitForEvent(client, "new_ticket");
await createHttpTester(srv.url).post("/api/tickets", { serviceType: "general" });
const evt = await received;
ok(evt.ticket);
client.close();
await srv.close();
});Le client socket (
socket.io-client,ws…) reste fourni par l'application : mjs-unit ne l'embarque pas, pour préserver le zéro-dépendance.waitForEventfonctionne avec n'importe quel émetteur exposanton(idéalementonce/off).
