From 1f68ca4cf73624d6a05f887e0bc008eae343bebc Mon Sep 17 00:00:00 2001 From: Aleksandr Ilin Date: Wed, 7 Oct 2026 07:27:45 -0700 Subject: [PATCH] test(bdd): npm run test:feature -- runs only those features npm test -- features/x.feature runs the whole suite: cucumber-js merges command-line paths with cucumber.mjs's features/**/*.feature. On this cluster that's hours and starves the CI runners. test:feature passes the files through a config whose paths are exactly those files. Co-Authored-By: Claude Opus 5.5 --- docs/project/testing.md | 10 ++++++++++ service/cucumber.feature.mjs | 13 +++++++++++++ service/package.json | 3 ++- service/scripts/test-feature.mjs | 11 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 service/cucumber.feature.mjs create mode 100644 service/scripts/test-feature.mjs diff --git a/docs/project/testing.md b/docs/project/testing.md index 6aa5a0b..3e91d73 100644 --- a/docs/project/testing.md +++ b/docs/project/testing.md @@ -77,6 +77,16 @@ npm install # pulls in thomas via a pinned github:DeepSpaceCartel/thomas comm npm test ``` +To run only some features, use `test:feature`: + +```bash +npm run test:feature -- features/request_validation.feature features/health.feature +npm run test:feature -- features/health.feature --parallel 1 +``` + +`npm test -- features/x.feature` runs the **whole** suite: cucumber-js merges +paths given on the command line with `cucumber.mjs`'s `features/**/*.feature`. + ### Developing against a local, unreleased Thomas checkout `service/package.json`'s `thomas` devDependency is pinned to a real commit diff --git a/service/cucumber.feature.mjs b/service/cucumber.feature.mjs new file mode 100644 index 0000000..0d1fe12 --- /dev/null +++ b/service/cucumber.feature.mjs @@ -0,0 +1,13 @@ +// `npm run test:feature -- features/.feature [...]`: only the given +// feature files. A plain `npm test -- ` runs EVERYTHING, because +// cucumber-js merges command-line paths with cucumber.mjs's +// features/**/*.feature (deprecated behavior it warns about) - which on a +// small cluster means hours, not minutes. +import base from './cucumber.mjs'; + +const files = (process.env.FEATURES ?? '').split(/\s+/).filter(Boolean); +if (files.length === 0) { + throw new Error('test:feature: name the feature files, e.g. npm run test:feature -- features/health.feature'); +} + +export default { ...base, paths: files }; diff --git a/service/package.json b/service/package.json index a5c5292..1eba341 100644 --- a/service/package.json +++ b/service/package.json @@ -31,7 +31,8 @@ "start": "node dist/index.js", "test": "NODE_OPTIONS='--import tsx' cucumber-js", "test:unit": "node --import tsx --import ./src/testing/setup.ts --test src/*.test.ts", - "test:coverage": "node --import tsx --import ./src/testing/setup.ts --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/testing/**' src/*.test.ts" + "test:coverage": "node --import tsx --import ./src/testing/setup.ts --test --experimental-test-coverage --test-coverage-include='src/**/*.ts' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/testing/**' src/*.test.ts", + "test:feature": "node scripts/test-feature.mjs" }, "dependencies": { "@fastify/swagger": "^9.8.1", diff --git a/service/scripts/test-feature.mjs b/service/scripts/test-feature.mjs new file mode 100644 index 0000000..80c10f4 --- /dev/null +++ b/service/scripts/test-feature.mjs @@ -0,0 +1,11 @@ +// Runs cucumber-js on just the feature files given as arguments (see +// cucumber.feature.mjs for why `npm test -- ` isn't enough). +import { spawnSync } from 'node:child_process'; + +const files = process.argv.slice(2).filter((a) => !a.startsWith('-')); +const flags = process.argv.slice(2).filter((a) => a.startsWith('-')); +const result = spawnSync('npx', ['cucumber-js', '-c', 'cucumber.feature.mjs', ...flags], { + stdio: 'inherit', + env: { ...process.env, FEATURES: files.join(' '), NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --import tsx`.trim() }, +}); +process.exit(result.status ?? 1);