Testing the Monolith (the Equivalence Suite)
Unit and Testcontainers integration tests plus the Newman contract collection that becomes the behavior-equivalence suite for every later extraction.
Chapter 9 catalogued the smells planted in examples/00-monolith/ —
the god service, the shared schema, the one sprawling transaction. Planting a
smell only has value if you can prove, later, that curing it didn’t
change what the system does. That proof needs two different kinds of test, and
this chapter builds both. The first kind is the ordinary three-tier pyramid
every Spring Boot codebase should have: unit tests for the logic, slice tests
for the HTTP layer, integration tests for the real database. The second kind
is new, and it is the one new idea this chapter exists to teach: a suite that
watches the monolith from outside, through its REST surface only, captured
before a single line is extracted — the behavior-equivalence suite that
Part 5 onward will re-run, unchanged, as the gate every extraction has to
clear.
The pyramid lives in examples/00-monolith/src/test/; the behavior-equivalence
suite lives in tooling/newman/. Both are already-committed code: this
chapter walks the code as it exists on disk.
Three tiers, one mvn verify
Thirteen test classes carry the pyramid: four Tier 1 unit-test classes against
the four services with real branching logic — InventoryServiceTest,
OrderServiceTest, PaymentServiceTest, ReviewServiceTest (shipping and
notification have no branching logic to unit-test), six Tier 2 @WebMvcTest
slices against the six REST controllers, and a mixed Tier 3 of two
@DataJpaTest repository tests plus one full-stack smoke test. Counting
methods rather than classes: forty-one plain @Test methods, plus one
@ParameterizedTest in PaymentServiceTest that expands into four more
executions at run time — forty-five test executions in total, every one of
them green on a clean mvn verify. That is a modest number for a production
codebase and exactly the right number for a reference monolith: enough
coverage per context to make the smells demonstrable, not so much that the
example stops being readable in one sitting.
The three tiers answer three different questions, and keeping them separate matters before looking at any one of them in detail:
- Tier 1 (unit) asks “does this one class’s logic do the right thing,” with every collaborator mocked out. No Spring context, no database, no network — these tests run in milliseconds and they are where branching logic gets its most exhaustive coverage.
- Tier 2 (slice) asks “does this one controller bind, validate, and
serialize correctly,” with the service layer mocked and only the web stack
—
@WebMvcTest— booted. These tests catch the mistakes unit tests structurally cannot: a wrong@RequestMapping, a validation annotation that doesn’t fire, a DTO field that doesn’t serialize the way the contract promises. - Tier 3 (integration) asks “does this actually work against a real Postgres,” with everything wired up — real Hibernate mappings, a real Flyway-migrated schema, real transactions. These are slow and few by design; they exist to catch exactly the class of bug that mocking Tier 1’s repositories would hide.
Maven keeps the three tiers in two separate build phases without a single line
of custom configuration, purely through a naming convention. Surefire — bound
to the default test phase — keeps its out-of-the-box include pattern,
**/*Test.java, which happens to match every Tier 1 and Tier 2 class and
none of the *IT.java files. Failsafe is bound explicitly to
integration-test and verify:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
mvn test therefore runs only the fast tiers; mvn verify runs all three,
and fails the build on a red integration test exactly the way it already
failed on a red unit test. The fragile bit: the
split is driven entirely by a filename suffix, not by an annotation or a
plugin configuration that inspects what a test actually does. SixContextsSmokeTest
is the sharpest illustration of that fragility inside this very codebase — it
boots a full Spring context against a throwaway Testcontainers Postgres,
which is unambiguously integration-tier work, but because its filename ends
in Test rather than IT, Surefire’s default pattern picks it up and it runs
in the fast test phase, not the integration-test phase, alongside
OrderRepositoryIT and InventoryRepositoryIT. It still runs, and mvn
verify still fails if it goes red, so nothing is broken — but a reader adding
a tenth integration-style test and naming it *Test out of habit would get
exactly this outcome without Maven ever complaining. The convention works
because it is consistent, not because it is enforced.
Tier 1 — Mockito service unit tests
OrderService gets the deepest unit coverage of any class in the monolith,
and that is a deliberate choice, not an accident of which file a test author
happened to open first. It is both the god service flagged as SMELL[ch.26]
and the single in-process transaction flagged as SMELL[ch.22] — the one
class that touches five of the monolith’s six bounded contexts in a single
method call. A class with that much responsibility concentrated in one place
is exactly where a regression is most expensive and least visible, so
OrderServiceTest exercises every branch placeOrder can take: the happy
path, out-of-stock, payment-declined, and customer-not-found, plus a
not-found case on the read side. Every collaborator — OrderRepository,
CustomerRepository, InventoryService, PaymentService, ShippingService,
NotificationService — is a Mockito mock; this test never opens a database
connection.
The payment-declined test is the one worth reading closely, because it proves something about ordering, not just about a thrown exception:
@Test
void placeOrder_paymentDeclined_throwsAfterInventoryReservedButBeforeShippingOrNotification() {
var command = new OrderCreate(
1L, List.of(new OrderCreate.Line("SKU-WIDGET-001", 1)), "CARD-DECLINE", "1 Test Way");
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(inventoryService.findBySkuOrThrow("SKU-WIDGET-001")).thenReturn(widget);
when(orderRepository.save(any(Order.class))).thenAnswer(invocation -> invocation.getArgument(0));
doThrow(new PaymentDeclinedException("Payment method 'CARD-DECLINE' was declined"))
.when(paymentService).charge(any(Order.class), anyLong(), eq("CARD-DECLINE"));
assertThatThrownBy(() -> orderService.placeOrder(command))
.isInstanceOf(PaymentDeclinedException.class)
.hasMessageContaining("declined");
// SMELL[ch.22]: inventory WAS reserved (in-memory) before the decline; in
// the real flow only the surrounding @Transactional rolls that back. This
// unit test proves the orchestration order, not the rollback itself — the
// rollback is exercised by the Testcontainers integration tier.
verify(inventoryService).reserve("SKU-WIDGET-001", 1);
verify(shippingService, never()).dispatch(any(), anyString());
verify(notificationService, never()).sendOrderConfirmation(any(), any());
}
The doThrow(...).when(paymentService).charge(...) setup makes charge blow
up exactly where it would in the real PaymentService, and the three
verify calls at the end are the actual assertion: inventory was told to
reserve stock (the method got that far), but shipping and notification were
never called, because the exception unwound the call stack before either
ran. That is the orchestration contract a unit test can check cheaply —
what happened before the exception, and what definitely didn’t happen after
it — and the comment left in the test is explicit about what it is not
proving: whether the in-memory reservation actually gets rolled back in the
database is a question only a real transaction can answer, which is exactly
why OrderRepositoryIT exists at Tier 3. That division of labor — Tier 1
proves sequence and collaboration, Tier 3 proves durability — recurs
throughout the suite. InventoryServiceTest follows the identical shape for
reserve’s own branching (sufficient stock decrements in place; insufficient
stock throws and leaves the quantity untouched; an unknown SKU throws a
different exception), and PaymentServiceTest uses a single
@ParameterizedTest with four string variants — CARD-DECLINE,
card-decline, DECLINE-ANYTHING, visa-Decline-test — to prove the demo
decline rule (any payment method containing “decline”, case-insensitively) in
one test method instead of four near-identical copies.
Tier 2 — @WebMvcTest controller slices
A unit test proves OrderService does the right thing when called correctly.
It says nothing about whether the HTTP layer calls it correctly — whether a
malformed JSON body gets rejected before it ever reaches the service, whether
a 201 carries the Location header a client is entitled to expect, whether
a thrown domain exception actually turns into the right status code. That is
@WebMvcTest’s job, and OrderControllerTest is the clearest example of it:
@WebMvcTest(OrderController.class)
@AutoConfigureMockMvc(addFilters = false)
class OrderControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockitoBean private OrderService orderService;
@Test
void placeOrder_validCommand_returns201WithLocation() throws Exception {
var command = new OrderCreate(
1L, List.of(new OrderCreate.Line("SKU-WIDGET-001", 2)), "CARD-VISA", "1 Test Way");
var dto = new OrderDto(
7L, 1L, OrderStatus.CONFIRMED, 3998L, Instant.parse("2026-01-07T12:00:00Z"),
List.of(new OrderDto.Item("SKU-WIDGET-001", 2, 1999L)));
when(orderService.placeOrder(any(OrderCreate.class))).thenReturn(dto);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(command)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/orders/7"))
.andExpect(jsonPath("$.status", is("CONFIRMED")))
.andExpect(jsonPath("$.totalCents", is(3998)));
}
}
@WebMvcTest(OrderController.class) boots only the web MVC machinery around
that one controller — not the full ApplicationContext, not a database, not
the other five controllers — and @MockitoBean swaps in a mock OrderService
so the test controls exactly what the controller sees back from its one
collaborator. @AutoConfigureMockMvc(addFilters = false) disables Spring
Security’s filter chain for this slice, a choice the class’s javadoc is
explicit about: order endpoints carry no authentication rule of their own, so
disabling filters here tests the controller in isolation from a security
concern that belongs to Review, not Order — and Review’s own write-path
authentication was tested the same way, with filters left on, in its own
controller test, before that test was deleted along with the rest of
Review’s code when it left the monolith in r02/S10. That contract didn’t
disappear with the test class: it now lives in the behavior-equivalence
suite’s “Review Context Contract” folder (unauthenticated POST returns
401, authenticated POST returns 201), run against
examples/02-review-service. The
other two tests in the class — an empty-items-list request returning 400
with a VALIDATION_FAILED body, and an unknown order id returning 404 with
a NOT_FOUND body — exercise the error-mapping path the same way: the
ResourceNotFoundException the service throws has to come back out as the
right status and the right documented error shape, and that mapping lives
entirely in the web layer, invisible to a Tier 1 test that never serializes
anything.
Tier 3 — @DataJpaTest and a real Postgres
The first two tiers prove the code is internally consistent. Neither one can
prove that Order’s JPA mappings actually resolve against Postgres, or that
a cascade persists child rows the way the entity’s annotations claim it will
— proving that requires a real database, and OrderRepositoryIT is where that
happens:
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryIT {
@Container
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("monolith").withUsername("monolith").withPassword("monolith");
@DynamicPropertySource
static void datasourceProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
@Test
void findById_seedOrder_resolvesCrossContextJoinsToCustomerAndInventoryItem() {
Order order = orderRepository.findById(1L).orElseThrow();
assertThat(order.getCustomer().getName()).isEqualTo("Ada Lovelace"); // SMELL[ch.18]
assertThat(order.getItems().get(0).getInventoryItem().getSku()) // SMELL[ch.18]
.isEqualTo("SKU-WIDGET-001");
}
}
@AutoConfigureTestDatabase(replace = Replace.NONE) is the one line that
makes this a Tier 3 test rather than a disguised Tier 1 test: Spring Boot’s
@DataJpaTest defaults to swapping in an in-memory database, and Replace.NONE
turns that default off so the real spring.datasource.* properties — pointed
at the Testcontainers Postgres by @DynamicPropertySource — actually get
used. Without that one annotation, the test would silently run against H2
instead, and every SMELL[ch.18] comment in it would be proving nothing
about Postgres at all. The two assertions marked SMELL[ch.18] are the real
payload: they prove the direct foreign-key joins from orders into the
shared customers and inventory_items tables actually resolve through
Hibernate’s @ManyToOne mappings against the Flyway-migrated schema — the
exact coupling Chapter 9 named as a smell, now pinned down by a test that will
have to be rewritten the day that coupling gets cured.
InventoryRepositoryIT does the equivalent work for the PESSIMISTIC_WRITE
lock query InventoryService#reserve depends on — a query Spring Data’s
method-name parser will happily compile against a mock but can only be proven
correct against a real lock manager.
Both repository tests run a brand-new PostgreSQLContainer per test class
rather than sharing one through a common base class, and the javadoc on
InventoryRepositoryIT explains why: a static container field shared across
two top-level test classes gets torn down by the first class’s afterAll,
breaking the second class’s connection. Owning the container per class costs
a few extra seconds of container start-up per class; sharing it costs a
flaky, order-dependent build. SixContextsSmokeTest takes the same
self-contained pattern one level up, booting the entire Spring context with
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) against its own
throwaway Postgres and hitting one REST endpoint per surviving bounded
context — order, inventory, payment, shipping, notification — plus a sixth
check that is really a decommission proof: a request to /api/reviews now
returns 404, confirming that Review’s controller, service, repository, and
entity were cleanly removed from the monolith once the strangler proxy’s
cutover became permanent, with nothing else in the five remaining contexts
depending on the package that used to be there.
How Testcontainers finds Postgres without a running stack
None of the three Tier 3 classes above configure a container runtime
anywhere in their source — there is no docker.host property file checked
into the repository, no environment-specific branch in the test code. That is
Testcontainers working as designed: at startup, its container-runtime client
looks for a reachable Docker Engine API along a short, ordered list of
places — the DOCKER_HOST environment variable first, then a
~/.testcontainers.properties override, then a handful of well-known local
socket paths — and connects to whichever one answers. On this project’s
toolchain that is Docker Engine’s own /var/run/docker.sock (Chapter 1,
“Prerequisites & the Toolchain,” installs docker-ce and adds you to the
docker group), so no DOCKER_HOST export is needed and every
new PostgreSQLContainer<>("postgres:16-alpine") call above just works — no
code in the monolith’s test tree knows or cares which engine answered. The
one caveat worth knowing before a test run mysteriously hangs: Testcontainers’
Ryuk resource-reaper, which guarantees a leaked container gets cleaned up even
after a crashed JVM, needs to reach the same socket; a non-default docker
context or a stray DOCKER_HOST pointing elsewhere is the usual cause, and
docker context use default the usual fix. Either way, the self-provisioning
promise holds: nobody has to run docker compose up before mvn verify — the
Postgres instance these tests need is thrown away and rebuilt on every run,
which is exactly what makes them trustworthy rather than merely convenient.
From a pyramid to a contract: why the equivalence suite is different
Everything above this line is white-box: it knows the monolith’s internals, calls its Java classes directly or through Spring’s test slices, and in Tier 3’s case inspects a real database it is free to reset between runs. That is the right shape for proving the monolith’s implementation is correct today. It is the wrong shape for the job Part 5 is about to need, which is proving that a completely different implementation — a Quarkus service, with its own schema, its own persistence layer, maybe its own language idioms — still behaves the way the monolith did. A white-box test can’t even compile against code that doesn’t share the monolith’s class names, let alone its database rows.
The technique this problem calls for has a name outside this book: characterization testing, sometimes called golden-master testing — write tests that pin down what a system actually does, observed from the outside, before you change anything about how it does it. The discipline is strict about sequencing: the suite gets written and run green against the unmodified monolith first, capturing its real, current, possibly-smelly behavior as the standard to hold every later version to — not the behavior a specification says it should have, and not the behavior a cleaner rewrite would produce if you got to design it from scratch. That is a deliberate, almost uncomfortable choice: if the monolith’s payment-decline path happens to roll back a reservation inside the exact same ACID transaction as the order write, the characterization suite captures that, warts and smells included, rather than some more idealized future behavior. The point of writing it before cutting anything is that it is the only artifact that can answer the question “did this extraction change behavior,” because it is the only thing in the project that recorded what the behavior actually was before the extraction happened. Write it after the fact — or worse, write it against what the new service does and call that the baseline — and there is nothing left to compare against; a regression and a planned improvement look identical, because nothing captured the “before” to tell them apart.
In this project that suite lives in tooling/newman/mea.postman_collection.json,
run with the newman CLI (Postman’s collection runner), and it earns the name
behavior-equivalence suite for a specific, critical reason: every
request in it targets a baseUrl collection variable rather than a hardcoded
host, so the exact same collection can point at the monolith on one run and
at a freshly extracted Quarkus service on the next, with not one assertion
edited in between. Its companion concept is the equivalence gate — the
CI and per-chapter check that says an extraction is “done” only when this
unchanged collection passes against it. Four scenario folders carry that
contract today: a smoke check that the target is reachable and serving seeded
inventory; a happy-path checkout that confirms 201 Created, the right total
and line items, a Location header, and — critically — that stock actually
drops by the ordered quantity afterward; an out-of-stock checkout that
confirms 409 Conflict with a documented OUT_OF_STOCK error body and
unchanged stock; and a payment-declined checkout that confirms 402
Payment Required and, again, unchanged stock — proving the same rollback
behavior OrderServiceTest’s comment flagged as untestable at the unit tier.
A fifth folder, “Review Context Contract,” is a forward reference: it already
specifies the exact shape of Review’s REST surface that Chapter 15’s
extraction will have to reproduce, written against the monolith’s existing
/api/reviews endpoints before that code moves anywhere.
Putting the same behavior next to its two different test-tier expressions makes the contrast concrete. Here is the payment-decline branch proven at Tier 1, by calling Java directly with every collaborator mocked, next to the same behavior proven by the behavior-equivalence suite, by sending a real HTTP request and reading only the response back:
// OrderServiceTest — white-box: calls Java directly, every collaborator mocked.
doThrow(new PaymentDeclinedException("Payment method 'CARD-DECLINE' was declined"))
.when(paymentService).charge(any(Order.class), anyLong(), eq("CARD-DECLINE"));
assertThatThrownBy(() -> orderService.placeOrder(command))
.isInstanceOf(PaymentDeclinedException.class)
.hasMessageContaining("declined");
verify(inventoryService).reserve("SKU-WIDGET-001", 1); // reserved...
verify(shippingService, never()).dispatch(any(), anyString()); // ...then stopped
// mea.postman_collection.json — black-box: a real POST, only the HTTP
// response and a captured collection variable are visible.
pm.test('Declined payment returns 402 Payment Required', function () {
pm.response.to.have.status(402);
});
var json = pm.response.json();
pm.test('Documented error contract: error=PAYMENT_DECLINED', function () {
pm.expect(json.error).to.eql('PAYMENT_DECLINED');
});
// ...a later request re-reads /api/inventory/{sku} and compares it to
// widgetStockBeforeDecline, a value this same collection captured earlier —
// proving the reservation really rolled back, without ever touching the DB.
The Mockito test can see — and verify — that shippingService.dispatch was
never called; it is instrumenting the Java call graph directly. The Newman
assertion can see none of that; it only sees a status code, a JSON body, and
a second HTTP response it chose to fetch itself. That restriction is the
whole point. A suite that only ever asks “what does the outside world see”
is one that a Quarkus rewrite with a completely different internal call graph
can still satisfy, because nothing in the suite depends on there being a
shippingService to call in the first place. Capturing the “before” value —
widgetStockBeforeDecline — as a collection variable rather than a hardcoded
number is what lets the same collection be re-run safely against a long-lived
service with real, mutating data, instead of needing a fresh database reset
before every run; that property is exactly what the equivalence gate needs
once it runs unattended in CI rather than once by hand.
The equivalence gate: what “done” means from here forward
This suite already has a track record in this project’s own history.
Chapter 7 walked the full agentic development lifecycle run against
Review’s extraction, and the behavior-equivalence suite built in this chapter
is the exact artifact it was gating against: sixteen assertions caught a
native-image serialization defect that every JVM-mode test run had missed,
and forty-nine assertions — run through the strangler proxy with the
monolith stopped outright — were the differential check that finally proved a
routing predicate was silently sending every request to the wrong backend.
Neither of those findings came from a new test suite written for the
occasion; both came from running this collection, unchanged, against a
moving target. That is the pattern every later extraction repeats: Chapter 15
cuts Review out first and is graded by the “Review Context Contract” folder
already sitting in the collection today; Notification, Inventory, Payment,
Shipping, and finally Order with its own gateway follow in Part 6 and beyond,
each one required to pass the same collection — extended with its own
scenario folder when its turn comes, never rewritten for the services that
came before it. The rule from build-plan.md Section G is blunt:
an extracted service is “done” only when it passes the same collection the
monolith passed, both as a per-chapter acceptance gate and as a check wired
into CI. A green suite is necessary evidence for every one of those cutovers.
Chapter 7 already showed, with real commits and a real routing bug, that it
is not sufficient evidence by itself — but that it is the thing that makes
the search for what else might be wrong possible in the first place, because
without it there would be nothing re-runnable to doubt.
Build, run, observe
cd examples/00-monolith && mvn verify
A clean run shows Surefire’s output first — the Tier 1 and Tier 2 classes,
including the smoke test despite its integration-grade setup — then Failsafe
picks up OrderRepositoryIT and InventoryRepositoryIT in the
integration-test phase, and verify fails the build if either tier is red.
To see the behavior-equivalence suite run against a live monolith:
# 1. Postgres for the running app (separate from the throwaway Testcontainers instances above)
docker compose --env-file .env up -d postgres
# 2. build and run the monolith; Flyway migrates and seeds automatically
cd examples/00-monolith && mvn -q -DskipTests package
SPRING_DATASOURCE_PASSWORD="$(grep ^POSTGRES_PASSWORD ../../.env | cut -d= -f2)" \
java -jar target/monolith.jar
# 3. in another shell, from the project root
demos/demo-equivalence.sh
A green run reports every scenario folder passing against http://localhost:8080
— the monolith baseline this entire suite is captured from, and the baseline
every later extraction gets measured against.
The listings in this chapter are the “before” monolith as it stood on the
reference/monolith-before branch: Spring Boot 3.5, Testcontainers 1.x and
postgres:16-alpine. The frozen shell on main has since moved to Spring Boot
4.1.1 (DRQ-077), where the same test reads new
PostgreSQLContainer("postgres:18.6-alpine") from Testcontainers 2’s
org.testcontainers.postgresql package and gets its TestRestTemplate from
spring-boot-resttestclient; the shape of the pyramid is unchanged.
Cross-check
The suite’s collection-variable design is itself testable without touching a
single assertion: run demos/demo-equivalence.sh twice in a row against the
same running monolith. If the suite had hardcoded stock levels instead of
capturing a widgetStockBeforeHappyPath/widgetStockBeforeDecline baseline on each pass, the
second run would fail — stock would already be lower than the number baked
into the collection from the first run’s checkout. Seeing both runs pass
identically is a cheap, independent confirmation that the suite is safe to
run repeatedly against a long-lived target, which is exactly the property the
equivalence gate needs once it is running unattended in CI rather than once
by hand during authoring.
What you learned
- The monolith’s test pyramid separates three concerns: Tier 1
Mockito unit tests prove branching logic and call ordering with nothing
real behind them; Tier 2
@WebMvcTestslices prove the HTTP binding, validation, and error-mapping a unit test structurally can’t see; Tier 3@DataJpaTestand@SpringBootTestclasses prove the same logic survives contact with a real, Flyway-migrated Postgres, provisioned fresh by Testcontainers on every run over the local Docker Engine socket. - Surefire and Failsafe split those tiers by filename convention alone
(
*Testvs*IT) — powerful because it needs zero configuration, fragile because nothing stops a slow, container-backed test from being named*Testand quietly running in the fast inner loop, asSixContextsSmokeTestitself does. - A white-box pyramid can prove a monolith’s implementation is correct, but it cannot survive being pointed at a different implementation. A characterization/golden-master suite captures what the system does observably, from outside, before anything is cut — which is what makes the behavior-equivalence suite portable across a Spring monolith and a Quarkus extraction with nothing in common internally.
- The equivalence gate — this exact collection, re-run unchanged — is already proven essential: it is the artifact Chapter 7 showed catching a real native-image bug and anchoring the differential test that caught a real routing bug. Every extraction from Chapter 15 forward inherits that same gate rather than getting a new one invented for it.
The monolith now has code, deliberate smells, and a suite that proves both. Part 4, starting with Chapter 11, “Strategic & Tactical DDD and Hexagonal,” turns to the harder question this chapter’s tests can verify but can’t answer on their own: given everything the smells and the behavior-equivalence suite now make visible, where exactly does the first cut go?
Verification status: the test and tooling code this chapter walks is real
and already committed — examples/00-monolith/src/test/ and
tooling/newman/ — and its green runs are recorded in this project’s own
commit ledger rather than re-executed for this chapter specifically: commit
e6d62bc (“add three-tier JUnit test layer to monolith… mvn verify
green”) and commit 6cb113e (“add behavior-equivalence suite (Newman);
49 assertions green on monolith”), both cited in Chapter 7. The test-method
and execution counts in this chapter (thirteen classes, forty-one @Test
methods, forty-five executions once the one parameterized test expands)
were counted directly against the files on disk during authoring, not copied
from an earlier summary — worth a quick mvn verify re-run on a real machine
before this count is treated as permanently fixed, since a future iteration
could add or remove a test without this prose noticing.