Add Cucumber for readable BDD-style integration tests (apps/api)

Coexists with Mocha (kept for unit-style tests) and Cypress (unchanged,
web e2e). Adds:

- apps/api/features/*.feature — Gherkin scenarios
- apps/api/features/step-definitions/*.steps.ts — step implementations
- apps/api/features/support/world.ts — per-scenario World, spins up the
  Express app in-process via createApp() + supertest (no real server
  needed, same approach as the existing Mocha health test)
- apps/api/cucumber.cjs — config, deliberately .cjs (not .js) to avoid
  the same ESM/CJS config-loading mismatch that broke
  apps/web/cypress.config.ts earlier
- `test:bdd` script (cross-env + tsx via NODE_OPTIONS=--import=tsx, for
  cross-platform ESM+TS loading)
- health.feature/steps as a working example, mirroring the existing
  Mocha health test so both suites cover the same behavior in their
  respective styles

CI: runs `pnpm --filter api test:bdd` alongside the existing test step.
README: documents the new test layer and the TS/ESM config-loading
caveat for future tool configs.

Verified locally: lint, mocha, cucumber, and full build all pass.
This commit is contained in:
Nicolas 2026-08-16 11:05:20 +02:00
parent 746100e257
commit 47e0477c40
8 changed files with 687 additions and 13 deletions

View file

@ -22,6 +22,7 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm lint - run: pnpm lint
- run: pnpm --filter api test - run: pnpm --filter api test
- run: pnpm --filter api test:bdd
- run: pnpm build - run: pnpm build
e2e: e2e:

View file

@ -4,7 +4,7 @@
Monorepo pnpm workspaces : Monorepo pnpm workspaces :
- `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé) - `apps/api` — backend Express/TypeScript (squelette générique : healthcheck, config env, Prisma non modélisé, tests Mocha + Cucumber/BDD)
- `apps/web` — frontend React/Vite/TypeScript (squelette générique, prêt à être embarqué par Capacitor plus tard) - `apps/web` — frontend React/Vite/TypeScript (squelette générique, prêt à être embarqué par Capacitor plus tard)
- `packages/shared` — code partagé entre `api` et `web` (types, schémas de validation, constantes) — vide pour l'instant - `packages/shared` — code partagé entre `api` et `web` (types, schémas de validation, constantes) — vide pour l'instant
@ -71,8 +71,34 @@ pnpm dev:web
pnpm lint # Biome (lint + format check) pnpm lint # Biome (lint + format check)
pnpm lint:fix # Biome --write pnpm lint:fix # Biome --write
pnpm test # tests unitaires/intégration (Mocha, apps/api) pnpm test # tests unitaires/intégration (Mocha, apps/api)
pnpm --filter api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, apps/api)
pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement) pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
pnpm build # build de tous les workspaces pnpm build # build de tous les workspaces
``` ```
La CI GitHub Actions (`.github/workflows/ci.yml`) exécute lint + tests + build sur chaque push/PR vers `main`, puis les tests e2e Cypress. La CI GitHub Actions (`.github/workflows/ci.yml`) exécute lint + tests + build sur chaque push/PR vers `main`, puis les tests e2e Cypress.
### Cucumber (apps/api)
Tests d'intégration lisibles en Gherkin, en complément de Mocha (qui reste pour les
tests unitaires purs) :
- `apps/api/features/*.feature` — scénarios en Given/When/Then (`health.feature` sert
d'exemple)
- `apps/api/features/step-definitions/*.steps.ts` — implémentation des steps
- `apps/api/features/support/world.ts` — contexte partagé entre les steps d'un
scénario (instancie l'app Express in-process via `createApp()`, comme le fait déjà
supertest côté Mocha — pas besoin de lancer un vrai serveur)
- `apps/api/cucumber.cjs` — config (extension `.cjs` volontaire, voir la remarque
TypeScript/ESM ci-dessous)
Pour ajouter un scénario : écrire le `.feature`, lancer `pnpm --filter api test:bdd`,
implémenter les steps manquants (Cucumber affiche des snippets tout prêts pour ceux
qui n'existent pas encore).
> **Piège TypeScript/ESM à connaître** (déjà rencontré avec `cypress.config.ts`) :
> les fichiers de config d'outils tiers qui font du chargement dynamique de TS
> (`cucumber.cjs`, `cypress.config.ts`…) sont sensibles au `"type": "module"` du
> `package.json`. `cucumber.cjs` évite le problème *pour sa propre config* en étant
> explicitement CommonJS ; les steps/world restent en `.ts` ESM classique et sont
> chargés via `tsx` (`NODE_OPTIONS=--import=tsx`, voir le script `test:bdd`).

11
apps/api/cucumber.cjs Normal file
View file

@ -0,0 +1,11 @@
// Explicit .cjs extension (not .js) so this loads as CommonJS regardless of
// the "type": "module" in package.json — avoids the same ESM/CJS config
// loader mismatch that broke apps/web/cypress.config.ts earlier.
module.exports = {
default: {
paths: ["features/**/*.feature"],
import: ["features/**/*.ts"],
format: ["progress-bar"],
formatOptions: { snippetInterface: "async-await" },
},
};

View file

@ -0,0 +1,12 @@
Feature: API health check
As a monitoring service
I want to query the API
So that I can verify it is up and responding correctly
Scenario: The API is available
When I send a GET request to "/health"
Then the response status should be 200
And the response body should be:
"""
{ "status": "ok" }
"""

View file

@ -0,0 +1,16 @@
import assert from "node:assert/strict";
import { Then, When } from "@cucumber/cucumber";
import request from "supertest";
import type { CustomWorld } from "../support/world.js";
When("I send a GET request to {string}", async function (this: CustomWorld, path: string) {
this.response = await request(this.app).get(path);
});
Then("the response status should be {int}", function (this: CustomWorld, status: number) {
assert.equal(this.response.status, status);
});
Then("the response body should be:", function (this: CustomWorld, expectedJson: string) {
assert.deepEqual(this.response.body, JSON.parse(expectedJson));
});

View file

@ -0,0 +1,18 @@
import { type IWorldOptions, World, setWorldConstructor } from "@cucumber/cucumber";
import type { Express } from "express";
import type request from "supertest";
import { createApp } from "../../src/app.js";
// Fresh Express app per scenario (in-process, via supertest — no server to
// spin up/tear down) plus the last HTTP response, available to every step.
export class CustomWorld extends World {
app: Express;
response!: request.Response;
constructor(options: IWorldOptions) {
super(options);
this.app = createApp();
}
}
setWorldConstructor(CustomWorld);

View file

@ -8,6 +8,7 @@
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"start": "node dist/server.js", "start": "node dist/server.js",
"test": "mocha", "test": "mocha",
"test:bdd": "cross-env NODE_OPTIONS=--import=tsx cucumber-js",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev" "prisma:migrate": "prisma migrate dev"
}, },
@ -18,10 +19,12 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@cucumber/cucumber": "^13.2.1",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^22.9.0", "@types/node": "^22.9.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"chai": "^5.1.2", "chai": "^5.1.2",
"cross-env": "^10.1.0",
"mocha": "^10.8.2", "mocha": "^10.8.2",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"supertest": "^7.0.0", "supertest": "^7.0.0",

File diff suppressed because it is too large Load diff