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:
parent
746100e257
commit
47e0477c40
8 changed files with 687 additions and 13 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -68,11 +68,37 @@ pnpm dev:web
|
||||||
## Qualité / Tests
|
## Qualité / Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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 web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
|
pnpm --filter api test:bdd # tests d'intégration BDD (Cucumber/Gherkin, apps/api)
|
||||||
pnpm build # build de tous les workspaces
|
pnpm --filter web e2e # tests e2e (Cypress, démarre le serveur dev automatiquement)
|
||||||
|
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
11
apps/api/cucumber.cjs
Normal 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" },
|
||||||
|
},
|
||||||
|
};
|
||||||
12
apps/api/features/health.feature
Normal file
12
apps/api/features/health.feature
Normal 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" }
|
||||||
|
"""
|
||||||
16
apps/api/features/step-definitions/health.steps.ts
Normal file
16
apps/api/features/step-definitions/health.steps.ts
Normal 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));
|
||||||
|
});
|
||||||
18
apps/api/features/support/world.ts
Normal file
18
apps/api/features/support/world.ts
Normal 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);
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
601
pnpm-lock.yaml
601
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue