Files
spring-boot-demo/actuator-in-production/docs/09-testing-actuator.md
Ankur Mhatre 958b401f0f Spring Boot startup time: bean-by-bean diagnosis, and one directory per post
Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable
Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup
and FlightRecorderApplicationStartup behind a system property, and a /diag/startup
endpoint that computes step self time -- the number /actuator/startup does not give
you and the one that names the actual culprits.

Captured under docs/output/: the step tree sorted both ways, the same startup as JFR
events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath
scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache
timings (6.93 s to 4.82 s). Post body and metadata live in post/.

Moves the existing Actuator project into actuator-in-production/ so the repository
holds one directory per article; the root README is now an index.
2026-09-05 00:17:37 +05:30

3.5 KiB

← 08 The diagnostics endpoint · 09 · Testing Actuator · README →

09 — Testing Actuator

Eight tests, and none of them assert the happy path. They pin the behaviour that would be expensive to rediscover after an upgrade.

Pin the surprises

ActuatorExposureTests runs with exposeall,secured — the widest exposure with real security — and asserts:

@Test
void heapdumpIsNotExposedEvenWithWildcardExposure() throws Exception {
    mvc().perform(get("/actuator/heapdump")).andExpect(status().isNotFound());
}

If a future Boot release changes the access default for heapdump, or if somebody adds management.endpoint.heapdump.access=unrestricted to a shared config, this test is where you find out. Same for shutdown.

@Test
void envMasksEveryValueRegardlessOfKeyName() throws Exception {
    // acme.partner.credential matches no password/secret/token pattern.
    ... .andExpect(jsonPath("$.property.value").value("******"));
}

@Test
void anonymousHealthCarriesNoComponentBreakdown() throws Exception {
    mvc().perform(get("/actuator/health"))
         .andExpect(jsonPath("$.components").doesNotExist());
}

That last one is the show-details: when-authorized contract from chapter 04, asserted rather than assumed. It is a one-line configuration change away from leaking your dependency map to anonymous callers.

Test the operational contract, not the indicator

HealthGroupTests:

@Test
void upstreamOutageMovesTheExternalApiIndicatorButNotLiveness() {
    upstream.set(UpstreamState.Mode.DOWN);
    assertThat(statusOf("externalApi")).isEqualTo(Status.DOWN);
    assertThat(statusOf("livenessState")).isEqualTo(Status.UP);
}

That is chapter 07 as an executable assertion. It fails if someone adds externalApi to the liveness group, which is the exact change that causes a fleet-wide restart loop.

@Test
void theExternalApiIndicatorRespectsItsTimeoutBudget() {
    upstream.set(UpstreamState.Mode.SLOW);   // the stub sleeps 30s
    ...
    assertThat(elapsedMs).isLessThan(5_000L);
}

Fails if someone removes the read timeout. A generous bound, because timing assertions on CI are how you get flaky suites — but 5 s versus 30 s is a wide enough gap to be safe.

DEFINED_PORT, and why

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)

Not the default MOCK environment. ExternalApiHealthIndicator makes a real HTTP call to this application's own stub controller, so a servlet container has to be listening on the port it was configured with. Under MOCK the indicator reports DOWN with a connection error and every assertion about upstream state is meaningless — which is precisely what happened on the first run of this suite.

RANDOM_PORT does not help here: the indicator resolves its URL at bean construction, before the server binds, so local.server.port is not available to it.

What is not tested

There is no test for the 60-second Kafka timeout. A test that takes a minute to pass does not belong in a suite people run before pushing — scripts/demo-kafka-timeout.sh captures it instead, and the transcript is committed. Some evidence is better as a script than as an assertion.


← 08 · 09 · README →