Which Test Suites Actually Run
1. Overview
A green tick is evidence only if a suite actually executed. Across the EMS repositories that is not uniformly true. One front-end repository’s test job installs and builds without running a single test unless the caller opts in. A WordPress plugin suite can exit with status 0 having executed nothing, byte-for-byte indistinguishable from a clean pass.
This page answers one question per repository — does the green check mean tests ran? — and names the local command that is authoritative when it does not.
The companion page for pipeline behaviour itself is CI and Release Pipeline Traps; the workflow architecture is described in GitHub Actions CI/CD.
2. Trust summary
| Repository | What a pull request actually runs | Authoritative local command |
|---|---|---|
admin-service |
The full |
|
registration-portal |
Front-end and back-end test jobs, gated by |
|
admin-portal |
A real Jest suite via |
|
membership-ui |
The shared front-end test job runs tests only when the caller passes |
|
database |
458 unit tests, gated by |
|
mcp-server |
15 unit tests, same gating. A thin suite over a small surface. |
|
spreadsheet-import |
One test. The gate is real and reports honestly; there is simply almost nothing behind it. Treat a green check here as evidence the build compiles, not that the readers work. |
|
WordPress plugins |
A PHPUnit job that can report success having executed zero tests. |
|
|
A pull request check and enforcement of that check are separate things. A repository can report a
|
3. admin-service: the pull request runs the full suite
pr-non-main.yml runs mvn verify — unit and integration — on every pull request that touches backend code, and reports the aggregated PR checks context that develop requires. The configuration and the reasoning behind each part of it are in Repository Rules.
Two properties are worth knowing before relying on it:
-
The check is binding on the automation and on any non-administrator.
enforce_adminsisfalseso the release automation can push directly to protected branches, which means an administrator can also merge past a red check deliberately. -
CI and a local run are not the same measurement. The CI job takes roughly twelve to eighteen minutes; the same suite on the development machine takes substantially longer, because the shared MySQL container and the Spring contexts are slower there. A local run that is slow is not evidence of a CI problem.
Use mvn verify, not mvn test — surefire excludes */*IT, so mvn test runs the unit tests and none of the integration tests. The full admin-service suite is roughly twelve hundred unit tests and twenty-eight hundred integration tests. A handful of skips are pre-existing @Disabled cases and are expected.
3.1. The failure class the check narrows but does not close
Two pull requests can each be green in isolation and produce a red develop when both land. Git merges them without conflict because they touch no common file; the semantic collision only exists in the merged result.
The pull_request event builds base merged with branch, so the check does catch this — but only for collisions that already existed when the branch was last built. The required check is deliberately non-strict, meaning a pull request is not forced to rebuild after the base moves, so the second of two concurrently-open pull requests can merge on the strength of a merge commit computed before the first one landed. The reasoning behind that choice is in Repository Rules.
The shape, constructed:
-
Branch A removes a value from an enumeration-keyed dispatch map and sweeps the tests that used that value merely as a vehicle.
-
Branch B, invisible to that sweep because it lives on another branch, adds new tests using the same value.
-
Merged, the dispatcher throws on an unhandled signal type and the suite is red.
If A merges while B’s check is already green, nothing recomputes B’s merge commit before B merges in turn.
The blast radius is larger than a red suite: push-dev chains test, package, docker, helm and deploy, so a failing test starves the dev environment of builds entirely for as long as develop stays red.
Two habits contain it:
-
When
developgoes red with no obvious culprit, find the first red run, not the one someone noticed. Later merges inherit the failure and look equally guilty.gh run list --branch develop --limit 30 -
When fixing such a collision, continue whatever sweep the first commit already performed — its commit message usually states the rule — rather than inventing a new reconciliation. Then check the open pull requests for a recurrence of the same shape before calling it done.
3.2. JaCoCo "Unknown block type" is a corrupted exec file, not a failure
A build can fail with zero test failures:
Failed to execute goal org.jacoco:jacoco-maven-plugin:report-integration
(post-integration-tests): Error while creating report:
Unknown block type ba
This means target/jacoco-it.exec is truncated, not that a test failed. It happens when an earlier Maven run on the same working tree was killed mid-write — a session restart, a Ctrl-C, an IDE restart — leaving a partial exec file that the next run appends to.
Diagnose by counting real failures in the log; if the count is zero, nothing failed:
grep -E 'Tests run:.*(Failures: [1-9]|Errors: [1-9])' build.log
Fix by clearing the exec files and re-running:
mvn -o clean verify
Always re-run from clean before reporting a red build caused this way.
3.3. Reading Maven logs
Strip ANSI escapes first or greps silently miss lines:
sed -e 's/\x1b\[[0-9;]*m//g' build.log > build.plain.log
The two summary lines are the Tests run: N, Failures: … lines with no - in <Class> suffix. The failsafe summary is emitted at [WARNING] level whenever anything is skipped, so a grep anchored on ^\[INFO\] finds only the surefire total and silently loses the integration-test total.
3.4. Coverage that is not coverage
A test can execute, pass, and still assert nothing about the behaviour it appears to cover. The recurring shape in admin-service is a controller test that constructs the controller directly and mocks the service it delegates to: the mock’s stubbed return makes the test green while saying nothing about how the service selects among candidates. It looks like coverage and is not, which is how selection regressions reach develop unnoticed.
Behaviour that lives in a service — instance selection, filtering, ordering — belongs in a database-backed integration test annotated @IntegrationTest, not in a controller unit test.
4. admin-service: no test reads the production configuration
src/test/resources/config/application.yml shadows src/main/resources/config/application.yml. Both resolve to config/application.yml on the classpath, and test resources win, so a @SpringBootTest context in admin-service reads nothing from the file the service actually ships.
The consequence is easy to state and easy to miss: deleting or mistyping a spring.* setting in the production config breaks production and leaves the entire suite green. There is no test that would notice.
Two things follow for anyone adding a spring.* setting to this service.
-
Mirror it into
src/test/resources/config/application.yml. Otherwise every integration test runs a different configuration than production does — which is its own hazard, quite apart from coverage, because the behaviour under test is not the behaviour that ships. -
Guard the shipped file directly. A test that reads the production YAML off disk (
src/main/resources/config/application.yml, relative to the module — Maven’s working directory) is the only thing that will fail when the setting is removed.
Both halves are needed and neither substitutes for the other:
| Test shape | What it can catch |
|---|---|
Parse the production YAML off disk |
The setting is missing from the artifact that ships. Cannot catch a key Boot does not recognise — it is only reading text. |
Bind the property in a Spring context |
A mistyped key. YAML accepts |
Worked example: SecurityFilterDispatcherTypesTest and SecurityFilterDispatcherTypesIT, added with Bug #1135.
|
When mirroring a setting, merge it into the existing block rather than adding a second one at the same level. A duplicate key under one mapping makes SnakeYAML reject the whole document, and the failure surfaces as |
5. membership-ui: the front-end check may not have run anything
5.1. The shared workflow’s test step is opt-in
The front-end test check is not proof a suite passed. The shared front-end test workflow in the parent-pom repository exposes a run-tests input that defaults to false, alongside a step that emits a "front-end tests were skipped" workflow warning so the gap is legible in the run summary. Until a consumer explicitly passes run-tests: true, the job installs dependencies and builds without executing a test.
Check whether the caller opted in before citing that check as evidence. Repositories are opted in individually as their suites go green; the default flips once the last one has.
Do not grep .github/workflows/ for ng test and conclude there is no test step. The call is a reusable-workflow reference:
uses: christhonie/event/.github/workflows/test-fe.yml@main
Follow reusable-workflow references before making any claim about what CI runs. Note also that the parent-pom repository commits workflow files straight to main and consumers reference @main, so a change there goes live for every consumer at once — see CI and Release Pipeline Traps.
5.2. The suite is largely red, so baseline before you attribute
Many of the generated specs were never repaired after their components were hand-customised, so a substantial fraction of suites fail on develop. A red run therefore proves nothing about your change until you have compared it against the branch baseline:
git stash push -u
npx ng test
git stash pop
npx ng test
Compare suite counts and test counts. Both matter, because of the next trap.
5.3. A spec that does not compile reports zero tests, not a failure
A spec file with a compile error is reported as one failed suite and zero tests run. A whole file of tests can therefore be silently dead while the summary looks like a single ordinary failure — for example a spec using a type it never imported, whose tests had never executed since the day they were written.
When the suite-count delta and the test-count delta disagree, a file is dead rather than failing. Fix the compile error before reading anything else in the run.
5.4. Run Jest through the Angular builder, never raw
jest.conf.js in membership-ui declares no transform and no preset. The TypeScript transform is injected by @angular-builders/jest from the angular.json configuration, so the config file is only half a configuration on its own.
Run tests as:
source ~/.nvm/nvm.sh
npx ng test --coverage=false -w=2 --test-path-pattern="<pattern>"
Running npx jest --config jest.conf.js directly fails misleadingly, in two stages: first a Babel version clash, because the Babel preset chain wants a newer @babel/core than the Angular version pins, and then TypeScript syntax errors such as "Missing initializer in const declaration", because babel-jest has been handed TypeScript with no TypeScript handling. Neither is a real dependency problem. Do not add package.json overrides to chase them — the lockfile is correct and the invocation is wrong.
Note also that npm test runs a pretest step of full-repository ESLint, which has pre-existing errors. Use npx ng test directly for suites, and compare ESLint findings against develop with the same stash technique rather than expecting a clean run.
6. admin-portal: a real Jest suite with two traps
admin-portal front-end tests run under Jest — not vitest, not Karma. The runner is @angular-builders/jest (npm test maps to ng test), configured by jest.conf.js with the jest-preset-angular preset and a jsdom environment.
source ~/.nvm/nvm.sh
CI=true npx ng test
6.1. Use Jest ambient globals, never vitest
describe, it and expect are ambient globals supplied by @types/jest. A spec that does this:
import { describe, expect, it } from 'vitest'; // wrong
fails to resolve the module, and the whole suite is reported as failed to run rather than as one bad assertion. Use jest.fn() and jest.Mock; never vi.*.
6.2. A green build does not mean the specs compile
ng build excludes specs — they are compiled under a separate tsconfig.spec.json. A spec can be badly broken while ng build passes cleanly. Verify specs with the Jest run, separately, every time.
6.3. paths-filter can merge spec debt unrun
The front-end test job is gated by dorny/paths-filter (see GitHub Actions CI/CD). A pull request that touches none of the matched paths skips the job entirely, so a broken spec — a vitest import, a stale assertion — can merge unrun and surface only on a later pull request that happens to re-trigger the job. The later author then inherits several failing suites they did not write.
Two mitigations: run the suite locally on any pull request that adds or edits specs regardless of what CI decides, and treat a skipped front-end check as unknown, never as a pass.
Where a spec pins UI structure — a hardcoded navigation order, for instance — the same array is often asserted in more than one spec file. Adding or reordering such a row requires updating every spec that pins it, not just the first one the search turns up.
7. WordPress plugins: PHPUnit can pass having run nothing
Suspect this whenever a PHP suite looks suspiciously fast or prints only a banner.
7.1. The direct-access guard kills the process
Every plugin class opens with the standard WordPress guard:
defined('ABSPATH') || exit;
Outside WordPress, ABSPATH is undefined, so the first time PHPUnit autoloads any plugin class the guard calls exit(). That terminates PHP before PHPUnit prints its results, and the process ends with status 0 — indistinguishable from a clean pass to anything reading the exit code.
The fix is a tests/bootstrap.php that defines ABSPATH before requiring the autoloader — the order is the whole point — wired in via phpunit.xml. Add a minimal get_option() stub returning the caller’s supplied default; loading real WordPress would require a database.
7.2. Two companion false greens
-
No
phpunit.xml. Runningvendor/bin/phpunit tests/with no configuration discovers zero tests and exits 0. This is a second, entirely independent false green, and it survives the bootstrap fix. -
PHPUnit 12 ignores doc-comment metadata. A
@dataProviderdoc-comment silently does nothing; it must be the attribute form,#[DataProvider('…')]. The same applies to the other annotations that moved to attributes.
7.3. A CI guard that actually works
Do not trust the exit code. Make PHPUnit write a JUnit log and fail the job when the log is missing or reports fewer than one test:
vendor/bin/phpunit --log-junit junit.xml
test -f junit.xml || { echo "no JUnit log written"; exit 1; }
count=$(grep -o 'tests="[0-9]*"' junit.xml | head -1 | grep -o '[0-9]*')
[ "${count:-0}" -ge 1 ] || { echo "PHPUnit ran no tests"; exit 1; }
Two packaging details when wiring this up: the common PHPUnit action writes no JUnit log unless explicitly asked, and the companion Composer action validates its dev input as the literal string "yes"/"no" and rejects a YAML boolean.
Finally, check when the plugin’s tests run. A release workflow that fires only when a pull request closes into main and packages without running tests is not a test gate at all; the tests need their own workflow on pull-request open.
8. Choosing test scope
Full suites are expensive and the development machine is frequently near capacity, often running other sessions' builds in parallel worktrees. Run the narrowest thing that answers the question.
| Question | Run |
|---|---|
Does this annotation, comment or import change compile? |
|
Does this unit-level behaviour hold? |
|
Does this integration case hold? |
|
Is the branch safe to push? |
|
Add -Dsurefire.failIfNoSpecifiedTests=false when pairing -Dit.test with a narrow or absent -Dtest.
Reserve the full gate for a change that genuinely warrants it: a broad refactor, a change to code read framework-wide, or the final check before pushing a branch with real behavioural change. In admin-service the pull request runs the same suite, so a local full run is a way to get the answer sooner rather than the only way to get it — see the admin-service section above.
For an OpenAPI contract change, the spec-extraction goal boots the full application context anyway, so it doubles as a smoke test.
|
Never kill Maven by a broad process pattern. A command such as Stop a build by its task id, or scope the pattern to the specific working directory. |
9. Related Documentation
-
CI and Release Pipeline Traps — pipeline and release behaviour that surrounds these suites
-
GitHub Actions CI/CD — workflow architecture, path filtering, the
PR checksaggregator and branch protection -
Development Workflow — GitFlow branching and release process
-
Maven POM Conventions — parent POM, profiles and the shared plugin set