Summary
Propose a structured approach to make integration-test coverage available in a local pipeline running in ≤1 minute on 4-core / 16 GB RAM hardware.
The proposal is modulith-as-primary in scope because that's the active development target. This scoping is narrower than a full split+modulith coverage, but the Java code under test (gateway routing, ZAAS auth, JWT validation, Eureka integration) is identical for both deployment topologies — they're run with the same excludeTags list and the same @Test classes. Most split-arch jobs in the current pipeline are functionally redundant with their modulith counterparts and can be deleted from CI as a side benefit.
What the local pipeline must cover is the configuration variants that exist in CI today — SAF provider, z/OSMF without JWT, Discovery basic auth off, unknown hostnames, Infinispan storage, and the legitimate test categories (registration, gateway routing, central registry). These each exercise different code paths.
What the CI pipeline must keep (cannot run locally in 1 minute): HA topology (multi-instance + chaos matrix), JGroups/Infinispan clustering, real mainframe providers (SAF, z/OSMF, ICSF), real OIDC provider, real Node.js/Python sample apps, and real container chaos scripts.
Background
Service modules (already fast, already passing locally)
| Module |
Test files |
Test methods |
Spring slices |
| gateway-service |
89 |
486 |
7 @SpringBootTest |
| discovery-service |
22 |
117 |
5 |
| api-catalog-services |
37 |
169 |
6 |
| caching-service |
33 |
308 |
6 |
| zaas-service |
72 |
548 |
8 |
| zaas-client |
7 |
80 |
— |
| apiml-security-common |
45 |
240 |
— |
| apiml-common |
26 |
103 |
— |
| apiml-tomcat-common |
11 |
60 |
— |
| common-service-core |
50 |
256 |
— |
| certificate-common |
1 |
17 |
— |
| onboarding-enabler-java |
7 |
49 |
— |
| TOTAL unit tests |
400 files |
2,433 methods |
|
These run as part of ./gradlew clean build. They use Mockito / pure unit tests / @WebMvcTest slices and do not require the gateway stack.
integration-tests/ module — the slow part
196 test files, ~406 test methods. Each implements TestWithStartedInstances, which calls FullApiMediationLayer.startServices() to spin up Gateway + Discovery + ZAAS + API Catalog + Caching + Mock Services + Discoverable Client via ProcessBuilder (local) or Docker (CI).
Current CI workflow — 29 jobs
integration-tests.yml runs jobs on GH-hosted runners with 15-min timeouts. Many jobs duplicate their modulith counterparts: CITests and CITestsModulith both call runContainerTests/runContainerModulithTests with the same excludeTags list (the only excludeTags difference is NonModulithTest, which is one or two tests). Same for CITestsRegistration vs CITestsRegistrationModulith (both run runRegistrationTests), and the DiscoveryBasicAuth/ZosmfWithoutJwt pairs.
Aside: collapsing the duplicate split/modulith pairs in CI itself saves ~50% of runner minutes and is a quick win independent of this issue.
Test categories in integration-tests/ — what exercises different code paths
The Java tests themselves don't care about deployment topology. The tags that do gate different code paths are:
| Configuration variant |
Tag |
Real services required |
Local-runnable embedded |
| Standard smoke (CITests, CITestsModulith) |
— |
full stack |
✓ @SpringBootTest + WireMock |
SAF auth provider (SAFAuthTest, SAFProviderTest) |
@SAFAuthTest, @SAFProviderTest |
Real SAF/ICSF |
✗ mainframe |
z/OSMF without JWT (ZosmfAuthTest) |
@zOSMFAuthTest |
real z/OSMF in ltpa mode |
✗ mainframe |
| Unknown hostnames |
@UnknownHostnamesTest |
unresolvable hostnames |
✓ embedded with bad hostname config |
Discovery basic auth (DiscoveryBasicAuthTest) |
@DiscoveryBasicAuthTest |
cert-verification-off config |
✓ embedded |
Registration (RegistrationTest) |
@RegistrationTest, @MultipleRegistrationsTest |
full stack |
✓ embedded |
| Caching service (CachingServiceTest, RedisReplica, RedisSentinel) |
@CachingServiceTest, @CachingServiceTest |
various storage backends |
In-memory: ✓; Redis: ✗ |
| Central registry (CentralRegistryModulith) |
@GatewayCentralRegistry |
2-gateway topology |
partial (5 of 7 embedded; some need 2 contexts) |
| Infinispan storage |
@InfinispanStorageTest |
JGroups cluster |
✗ requires 2+ caching instances |
| HA chaotic (HA × matrix) |
@HATest, @ChaoticHATest |
multi-process + chaos scripts |
✗ Docker chaos only |
| Sticky/deterministic LB |
@StickySessionLbHaTest, @DeterministicLbHaTest |
multi-instance gateway |
✗ Docker HA only |
| OIDC (OidcOauth2Test, ~30 tests) |
@OidcOauth2Test |
real OIDC provider |
partial — about 10 of 30 can be embedded with mock provider |
| z/OS provider (SafLoginTest, ZosmfLoginTest, ~12 tests) |
@GeneralAuthenticationTest |
real mainframe |
✗ mainframe |
| Node.js / Python sample apps |
@NodeEnablerTest, @PythonEnablerTest |
real Node.js / Python runtimes |
✗ |
| Penetration (JwtPenTest) |
— |
adversarial envs |
✗ complex state |
| WebSocket / STOMP / SSE proxy (WebSocketProxyTest, StompProxyTest, ServerSentEventsProxyTest) |
@WebsocketTest |
real broker + handshake |
partial (2-3 of 9 WS tests embedded; STOMP/SSE: ✗) |
| Service ID prefix replacer (ServicePrefixReplacerIntegrationTest) |
— |
full stack |
✓ embedded |
| Passticket / ZAAS schemes (PassticketSchemeTest, ZoweJwtSchemeTest, etc.) |
@GatewayServiceRouting |
full stack |
✓ embedded with WireMock |
| ZAAS error paths (ZaasNegativeTest) |
— |
ZAAS only |
✓ pure embedded |
| Personal Access Token (AccessTokenServiceTest, PATWithAllSchemesTest) |
— |
full stack |
partial — provider-agnostic parts embedded; provider-specific: ✗ |
| Functional Gateway (GatewayRoutingTest, VersionTest, PageRedirectionTest, GatewayAuthenticationTest, InMemoryRateLimiterIntegrationTest, StaticClientRoutingEndpointsTest) |
various |
full stack |
✓ all embedded |
| API Catalog (ApiCatalogEndpointIntegrationTest, ApiCatalogLoginIntegrationTest, ApiCatalogAuthenticationTest) |
@CatalogTest |
full stack |
✓ all embedded |
| Discovery service (DiscoveryServiceAuthenticationTest, DiscoveryServiceRegistrationTest, DiscoveryBasicAuthProtectionTest) |
— |
full stack |
✓ all embedded |
| Caching service (CachingAuthenticationTest, CachingStorageTest) |
— |
full stack + backends |
In-memory variant: ✓; Redis/VSAM: ✗ |
Why This Issue
Today, the dev loop is:
- Push PR
- Wait ~10-15 minutes for
integration-tests.yml to finish
- Find out a 5-line change broke gateway routing
A fast local pipeline would catch most regressions before pushing. The 4-core / 16 GB constraint is what a developer's laptop has, and what a small CI runner has.
Proposed Solution
Goal
./gradlew localFast runs ≤60 seconds on 4-core / 16 GB hardware
- Catches ~85% of CI regressions locally (excluding HA/chaos/mainframe-specific)
- Decreases CI cost by deleting redundant split/modulith duplicate jobs (independent win)
Pattern (illustrative for gateway-service)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"apiml.enabled=false",
"spring.profiles.active=embedded"
},
classes = GatewayServiceApplication.class
)
@Tag("GatewayServiceRouting")
class GatewayRoutingEmbeddedTest {
@LocalServerPort int port;
@RegisterExtension
static WireMockExtension discoverableClientMock = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();
@DynamicPropertySource
static void register(DynamicPropertyRegistry r) {
r.add("apiml.service.discoveryServiceUrls", () ->
"http://localhost:" + discoverableClientMock.getPort() + "/eureka");
}
@ParameterizedTest
@CsvSource({
"/apiml1" + DISCOVERABLE_GREET,
DISCOVERABLE_GREET,
})
void testRoutingWithBasePath(String basePath) {
given().relaxedHTTPSValidation()
.get("https://localhost:" + port + basePath)
.then().statusCode(200);
}
}
apiml.enabled=false disables Eureka peer replication (the source of the BuildAndTest 35-min timeout). WireMock stubs /eureka/apps and the discoverable-client routes.
Phase 1 — Configuration (2 dev-days)
- Apply
apiml.enabled=false and parallelTests plugin to all service-module test tasks
- Add
application-embedded.yml profile to each service
- Add WireMock test dependency
- Add
@Tag("CIOnly") to integration tests that must stay CI-only (HA/chaos/mainframe)
// per-service build.gradle
test {
forkEvery 50
maxParallelForks 2
useJUnitPlatform {
if (!project.hasProperty('runSlowTests')) {
excludeTags 'SlowTests', 'HATest', 'ChaoticHATest',
'InfinispanJGroupStabilityTest', 'CachingServiceTest',
'InfinispanStorageTest', 'RedisTest'
}
}
}
Phase 2 — Move tests into service modules (5 dev-weeks)
Per-service move plan with effort estimates. All counts are modulith-as-primary; split-arch equivalents of these tests are deleted from CI as a side effect.
| Service |
Tests moved |
Effort |
Net CI jobs removed |
| gateway-service |
~46 (gateway routing, X-509 scheme, Passticket scheme, JWT scheme, CORS, page redirect, rate limit, central registry, X-Forward, version, static client) |
1.5 weeks |
delete GatewayProxy split, GatewayCentralRegistry split, CITestsServicePrefixReplacer split |
| discovery-service |
~8 (auth, basic auth protection, multi-registration) |
3-4 days |
delete CITestsDiscoveryBasicAuth split |
| api-catalog-services |
~18 (endpoints, login, auth variants) |
1 week |
(none — modulith has these) |
| caching-service |
~16 (auth, in-memory storage; defer Redis/VSAM) |
3-4 days |
delete CITestsWithRedisReplica, CITestsWithRedisSentinel (or fold into HA chaos) |
| zaas-service |
~28 (negative, PAT, generic login, OIDC core subset) |
2 weeks |
delete CITestsZaas split |
| zaas-client |
~14 (E2E client library with embedded ZAAS) |
3 days |
— |
| onboarding-enabler-java |
~3 (token validation) |
1 day |
— |
| Subtotal |
~133 tests |
~6.5 weeks |
|
For each move:
- Pull test logic into the new module
- Replace
TestWithStartedInstances with @SpringBootTest(webEnvironment = RANDOM_PORT) + WireMock
- Replace
getUriFromGateway() with localhost:${localServerPort}
- Old test file gets
@Disabled("Moved to <target-module>/.../...")
- After 2-4 weeks CI proves both versions agree, delete the CI counterpart
Phase 3 — Tier 2 multi-service contracts in single JVM (1.5 dev-weeks)
For flows that genuinely need gateway + z/OS auth + mock service working together, add new embedded tests that boot 2-3 services in one JVM via SpringApplication.run() with separate ports. Coverage targets where existing integration tests are too coarse:
MultiServiceJwkRotationTest — JWKS endpoint in ZAAS, key rotation, gateway validates
CrossServiceOauthFlowTest — gateway → ZAAS → back to gateway with OAuth2
ApiCatalogFullChainTest — Catalog → discovery → gateway → mock service
StaticRefreshEndToEndTest — POST /static-api/refresh → catalog → discovery reload
~6 new tests, all <300ms each once contexts are loaded.
Phase 4 — Local pipeline + CI sync (1 dev-week)
# .github/workflows/local-fast.yml
name: Local Fast (1-min target)
on: [push, pull_request]
jobs:
fast:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: {java-version: 17, distribution: semeru}
- run: ./gradlew test :integration-tests:tier2Suite --max-parallel-fork=2
Plus a localFast task that aggregates module-level tests with apiml.enabled=false.
Test Inventory After Migration
| Source today |
Local runs |
CI runs |
Standard smoke (~120 tests via runContainerModulithTests) |
✓ Embedded |
✓ kept for parity |
| SAF provider tests (~10) |
✗ |
✓ |
| ZOSMF without JWT (~5) |
✗ |
✓ |
| Unknown hostnames (~3) |
✓ |
✓ |
| DiscoveryBasicAuth (~3) |
✓ |
✓ |
| Registration (~3) |
✓ |
✓ |
| CentralRegistry (~7) |
5 of 7 |
all 7 |
| Caching service (in-memory part) (~10) |
✓ |
✓ |
| Caching service (Redis part) (~5) |
✗ |
✓ |
| Infinispan (~3) |
✗ |
✓ needs JGroups |
| HA + chaos matrix (~5 × matrix) |
✗ |
✓ |
| LB HA — sticky, deterministic (~10) |
✗ |
✓ |
| OIDC provider (~30) |
~10 with mock |
all 30 |
| ZAAS mainframe auth (~12) |
✗ |
✓ |
| Node/Python sample apps (~5) |
✗ |
✓ |
| Penetration (~variadic) |
✗ |
✓ |
| WebSocket proxy (~9) |
2-3 |
all 9 |
| Service ID prefix replacer (~2) |
✓ |
✓ |
| Passticket / ZoweJwt schemes (~17) |
✓ |
✓ |
| ZAAS error paths (~6) |
✓ |
✓ |
| PAT provider-agnostic (~9) |
partial |
✓ |
| Functional Gateway (~15) |
✓ |
✓ |
| API Catalog (~18) |
✓ |
✓ |
| Discovery service (~8) |
✓ |
✓ |
| Multiple-Registrations (~1) |
✓ |
✓ |
| Net result |
~150 tests |
~330 tests |
CI Job Reduction (Independent Win)
The 22-job matrix collapses. Suggested new workflow:
| New job |
What |
Old equivalent |
ModulithFast |
Standard smoke, embedded configs |
CITestsModulith + many |
ModulithAuthProviders |
SAF + ZOSMF, real mainframe |
CITestsModulithSAFProviderHA, CITestsZosmfWithoutJwtModulith |
ModulithHAChaotic |
HA + chaos matrix |
CITestsModulithHA × matrix |
ModulithInfinispan |
JGroups mesh |
CITestsModulithWithInfinispan |
ModulithSpecialty |
UnknownHostnames, CentralRegistry, BasicAuthOff, NodePython |
multiple modulith jobs |
| (deleted) |
— |
all split-arch jobs (functionally redundant) |
Net: 5 jobs instead of 22, each with the same coverage but ~80% lower runner-minutes.
Acceptance Criteria
What Stays CI-Only
Cannot run locally in 1 minute due to fundamental multi-process / external-system constraints:
| Category |
Why |
authentication/providers/Saf*, Zosmf* |
Real mainframe required |
ha/* (chaos, restart, replication) |
Multi-process / Docker / chaos scripts |
zos/* |
Real mainframe |
penetration/JwtPenTest |
Adversarial envs |
discoverable-client/*Node*, *Python* |
Real Node.js / Python runtimes |
proxy/StompProxyTest, ServerSentEventsProxyTest |
Real brokers / streams |
proxy/WebSocketProxyTest (most) |
Real WS handshake timing |
| Redis/VSAM storage variants |
Real backends |
| OIDC tests against real provider (~20 of 30) |
Real OIDC |
| HA chaos matrix |
Docker multi-instance + chaos scripts |
| Infinispan JGroup stability |
Multi-node JGroups mesh |
Memory & CPU Budget on 4-core / 16 GB
| Process |
Heap |
| Gradle daemon |
256 MB |
| 4 forked test JVMs (1.5 GB each) |
6.0 GB |
| OS + tooling |
1.0 GB |
| Total |
~7.25 GB / 16 GB ✓ |
CPU: most tests are I/O-bound (waiting for localhost HTTP response), so even single core works. --max-parallel-fork=2 avoids thrash.
Risks & Open Questions
- WireMock stubbing has bugs not caught by CI. Mitigation: keep both versions for 2-4 weeks before deleting CI counterparts.
- Some tests fundamentally cannot embed (multi-instance HA, real mainframe). Mitigation: documented in this issue, kept CI-only.
- Spring context caching not always effective with
@DirtiesContext. Mitigation: use forkEvery, max-parallel-fork=2 to recycle JVMs.
- Local pipeline ≠ production behavior. Some modules (like the
keyring:// SAF path) can only be exercised on z/OS. Mitigation: accept that local catches a subset, CI catches the rest.
Effort Estimate
| Phase |
Effort |
| 1 — Configuration |
2 dev-days |
| 2 — Move tests per service |
6.5 dev-weeks (incl. parity verification, then CI cleanup) |
| 3 — Tier 2 multi-service |
1.5 dev-weeks |
| 4 — Local pipeline + CI consolidation |
1 dev-week |
| Total |
~8 dev-weeks |
Labels
enhancement
architecture
size/L
References
Summary
Propose a structured approach to make integration-test coverage available in a local pipeline running in ≤1 minute on 4-core / 16 GB RAM hardware.
The proposal is modulith-as-primary in scope because that's the active development target. This scoping is narrower than a full split+modulith coverage, but the Java code under test (gateway routing, ZAAS auth, JWT validation, Eureka integration) is identical for both deployment topologies — they're run with the same
excludeTagslist and the same@Testclasses. Most split-arch jobs in the current pipeline are functionally redundant with their modulith counterparts and can be deleted from CI as a side benefit.What the local pipeline must cover is the configuration variants that exist in CI today — SAF provider, z/OSMF without JWT, Discovery basic auth off, unknown hostnames, Infinispan storage, and the legitimate test categories (registration, gateway routing, central registry). These each exercise different code paths.
What the CI pipeline must keep (cannot run locally in 1 minute): HA topology (multi-instance + chaos matrix), JGroups/Infinispan clustering, real mainframe providers (SAF, z/OSMF, ICSF), real OIDC provider, real Node.js/Python sample apps, and real container chaos scripts.
Background
Service modules (already fast, already passing locally)
@SpringBootTestThese run as part of
./gradlew clean build. They use Mockito / pure unit tests /@WebMvcTestslices and do not require the gateway stack.integration-tests/module — the slow part196 test files, ~406 test methods. Each implements
TestWithStartedInstances, which callsFullApiMediationLayer.startServices()to spin up Gateway + Discovery + ZAAS + API Catalog + Caching + Mock Services + Discoverable Client viaProcessBuilder(local) or Docker (CI).Current CI workflow — 29 jobs
integration-tests.ymlruns jobs on GH-hosted runners with 15-min timeouts. Many jobs duplicate their modulith counterparts:CITestsandCITestsModulithboth callrunContainerTests/runContainerModulithTestswith the sameexcludeTagslist (the onlyexcludeTagsdifference isNonModulithTest, which is one or two tests). Same forCITestsRegistrationvsCITestsRegistrationModulith(both runrunRegistrationTests), and the DiscoveryBasicAuth/ZosmfWithoutJwt pairs.Test categories in
integration-tests/— what exercises different code pathsThe Java tests themselves don't care about deployment topology. The tags that do gate different code paths are:
@SpringBootTest+ WireMockSAFAuthTest,SAFProviderTest)@SAFAuthTest,@SAFProviderTestZosmfAuthTest)@zOSMFAuthTestltpamode@UnknownHostnamesTestDiscoveryBasicAuthTest)@DiscoveryBasicAuthTestRegistrationTest)@RegistrationTest,@MultipleRegistrationsTest@CachingServiceTest,@CachingServiceTest@GatewayCentralRegistry@InfinispanStorageTest@HATest,@ChaoticHATest@StickySessionLbHaTest,@DeterministicLbHaTest@OidcOauth2Test@GeneralAuthenticationTest@NodeEnablerTest,@PythonEnablerTest@WebsocketTest@GatewayServiceRouting@CatalogTestWhy This Issue
Today, the dev loop is:
integration-tests.ymlto finishA fast local pipeline would catch most regressions before pushing. The 4-core / 16 GB constraint is what a developer's laptop has, and what a small CI runner has.
Proposed Solution
Goal
./gradlew localFastruns ≤60 seconds on 4-core / 16 GB hardwarePattern (illustrative for gateway-service)
apiml.enabled=falsedisables Eureka peer replication (the source of the BuildAndTest 35-min timeout). WireMock stubs/eureka/appsand the discoverable-client routes.Phase 1 — Configuration (2 dev-days)
apiml.enabled=falseandparallelTestsplugin to all service-module test tasksapplication-embedded.ymlprofile to each service@Tag("CIOnly")to integration tests that must stay CI-only (HA/chaos/mainframe)Phase 2 — Move tests into service modules (5 dev-weeks)
Per-service move plan with effort estimates. All counts are modulith-as-primary; split-arch equivalents of these tests are deleted from CI as a side effect.
For each move:
TestWithStartedInstanceswith@SpringBootTest(webEnvironment = RANDOM_PORT)+ WireMockgetUriFromGateway()withlocalhost:${localServerPort}@Disabled("Moved to <target-module>/.../...")Phase 3 — Tier 2 multi-service contracts in single JVM (1.5 dev-weeks)
For flows that genuinely need gateway + z/OS auth + mock service working together, add new embedded tests that boot 2-3 services in one JVM via
SpringApplication.run()with separate ports. Coverage targets where existing integration tests are too coarse:MultiServiceJwkRotationTest— JWKS endpoint in ZAAS, key rotation, gateway validatesCrossServiceOauthFlowTest— gateway → ZAAS → back to gateway with OAuth2ApiCatalogFullChainTest— Catalog → discovery → gateway → mock serviceStaticRefreshEndToEndTest— POST/static-api/refresh→ catalog → discovery reload~6 new tests, all <300ms each once contexts are loaded.
Phase 4 — Local pipeline + CI sync (1 dev-week)
Plus a
localFasttask that aggregates module-level tests withapiml.enabled=false.Test Inventory After Migration
runContainerModulithTests)CI Job Reduction (Independent Win)
The 22-job matrix collapses. Suggested new workflow:
ModulithFastModulithAuthProvidersModulithHAChaoticModulithInfinispanModulithSpecialtyNet: 5 jobs instead of 22, each with the same coverage but ~80% lower runner-minutes.
Acceptance Criteria
./gradlew localFastruns locally on 4-core / 16 GB in ≤60 secondsintegration-tests/tests are also executed bylocalFast, hosted in their respective service modules@Disabled-marked moved tests remain runnable in CI for 2-4 weeks before removal to confirm correctness.github/workflows/local-fast.ymlhas 3-minute timeoutgit pushWhat Stays CI-Only
Cannot run locally in 1 minute due to fundamental multi-process / external-system constraints:
authentication/providers/Saf*,Zosmf*ha/*(chaos, restart, replication)zos/*penetration/JwtPenTestdiscoverable-client/*Node*,*Python*proxy/StompProxyTest,ServerSentEventsProxyTestproxy/WebSocketProxyTest(most)Memory & CPU Budget on 4-core / 16 GB
CPU: most tests are I/O-bound (waiting for localhost HTTP response), so even single core works.
--max-parallel-fork=2avoids thrash.Risks & Open Questions
@DirtiesContext. Mitigation: useforkEvery,max-parallel-fork=2to recycle JVMs.keyring://SAF path) can only be exercised on z/OS. Mitigation: accept that local catches a subset, CI catches the rest.Effort Estimate
Labels
enhancementarchitecturesize/LReferences
/home/balda/engineering-team/architect/local-ci-plan.mdandper-service-test-plan.md(architecture working notes).