Lecture Slides

Chapter 7: Testing as a Design Practice

SE506: Object Oriented Development and Agile Practices

Dr. Polla Abdulhamid Fattah
Department of Computer Science & Artificial Intelligence
University of Kurdistan Hewlêr (UKH)

Chapter Roadmap (1 of 3)

  • 7.1 What does a test actually show? Behaviour and test evidence
    • Executable availability examples; observe result and state together.
  • 7.2 What a test looks like: Arrange–Act–Assert
    • Assertions that can be wrong; fresh, small setup.
  • 7.3 Red, green, refactor: one cycle in detail
    • One recorded cycle, and why the first failure must be the intended one.
  • 7.4 Edges, clocks and a small lending flow
    • Boundary values, a controlled clock, a bounded lending flow.

Chapter Roadmap (2 of 3)

  • 7.5 Test doubles: dummies, stubs, fakes, spies and mocks
    • Name the role a substitute plays, then add Mockito for one interaction.
  • 7.6 What testing tells you about design
  • 7.7 Tests that look good but are weak or brittle
    • Coverage is not correctness; a mutation experiment; a mutant that survives.
  • 7.8 Preparing the Milestone 2 evidence

Chapter Roadmap (3 of 3)

  • 7.9 Keeping implementation and expected behaviour apart, with AI separate implementation from expected behaviour.
  • 7.10 Testing habits that mislead
  • 7.11 Try it yourself: a test-first change and Milestone 2 evidence
    • Steps 1 to 9; steps 8 and 9 on your team’s project.
  • 7.12 Questions to think with, and midterm practice
    • A five-task practice set for Chapters 2 to 7.
  • What to take away, and what comes next
    • Chapter 8: construction and compatibility patterns.

Learning Outcomes (1 of 2)

By the end of this chapter, you should be able to:

  1. Translate acceptance examples into focused tests with meaningful expected outcomes.
  2. Demonstrate red-green-refactor and explain why the first failure is the intended one.
  3. Control data, time and collaborators to make tests repeatable.

Learning Outcomes (2 of 2)

  1. Choose a useful test double and distinguish state checks from interaction checks.
  2. Diagnose weak or brittle tests and explain the limits of passing evidence.
  3. Assemble MVP, testing, demonstration and retrospective evidence for Milestone 2.

Module Connection

  • Supports LO3; Milestone 2 preparation.
  • The midterm practice near the end draws on Chapters 2 to 7 (Weeks 1 to 6).
  • No new assessment weights, deadlines or marking rules.
  • Readings: the testing chapter and “A Programming Episode” in Martin, and Part I of Beck.

Milestone 2 Checklist: Use It From the Start (1 of 2)

Milestone 2 falls in this week. By your demonstration you need:

  1. A working MVP flow that someone else can run from written instructions.
  2. Acceptance examples mapped to named tests.
  3. One red-green-refactor sequence recorded from your own code.
  4. A short rationale for your object-oriented and SOLID choices.
  • Start with items 1 and 3.

Milestone 2 Checklist (2 of 2)

  1. A justified test double or controlled clock.
  2. A build result for the revision you demonstrate.
  3. Real review feedback and a short retrospective with a next action.
  4. A statement of what the MVP does not do.
  • Section 7.8 explains each item.
  • The checklist is preparation. The module leader’s approved Milestone 2 requirements remain authoritative.

Where You Start and the Tools

  • You start with: Chapter 6’s Java 17 application, explicit collaborator construction, student and staff policies, local notification outputs. The earlier executable checks remain available.
  • New tools: Maven, JUnit Jupiter and Mockito, with an explicitly versioned configuration in the companion.
  • Core reading 7.1 to 7.8. Section 7.5 introduces hand-written doubles before Mockito.
  • Lab: steps 1 to 6, 8 and 9. Steps 1 to 6 on the book’s project; **steps 8 and 9 on your team’s project.

Opening Scenario: A Green Build That Is Wrong

  • A developer adds the availability enquiry and reports the build is green.
  • At the demonstration the officer asks about a reserved camera and receives “available”.
  • The test had checked only that the method returned a boolean without throwing.

Three Plausible, Wrong Implementations

  • One passes because the test only checks “no exception”.
  • Another rejects every enquiry and still passes the overlap example.
  • A third accepts adjacent dates incorrectly because it treats the earlier reservation’s end as inclusive.
  • Each looks plausible when its tests ask incomplete questions.

What Testing Is For

  • Not to accumulate green indicators.
  • To create evidence about specific behaviour, reveal misunderstandings and support changes in design.
  • This chapter turns the availability examples of Chapters 3 and 4 into executable tests, demonstrates a complete red-green-refactor cycle, and uses controlled collaborators.

7.1 What Does a Test Actually Show?

Behaviour and test evidence

The Examples Are Now Implemented

  • Chapter 3 proposed US-02: an officer inspects availability without creating a reservation.
  • Chapter 4 refined five acceptance examples.
  • Now they become executable.

AQ Examples (1 of 2)

ExampleSetup and actionRequired observation
AQ-01Empty schedule; enquire about CAM1 for a valid intervalAvailable; records and notifications unchanged
AQ-02CAM1 reserved [20 Oct, 22 Oct); enquire [21 Oct, 23 Oct)Unavailable; no side effects
AQ-03Same booking; enquire [22 Oct, 23 Oct)Available because the end is exclusive

AQ Examples (2 of 2)

ExampleSetup and actionRequired observation
AQ-04Same booking; enquire about MIC1 for the same intervalAvailable for the different sample item
AQ-05Equal or reversed intervalIdentifiable invalid-input outcome; no side effects
  • All dates are in 2026.

The API Decision

  • The selected Java API is isAvailable(itemId, start, endExclusive), returning a boolean for a valid interval.
  • Blank or null item identifier: IllegalArgumentException.
  • Equal or reversed dates: IllegalArgumentException.
  • Null dates: NullPointerException through LoanPeriod.
  • These choices settle the earlier open API question for this teaching version.

What Availability Means

  • No conflicting reservation for the item and interval.
  • It does not mean a particular borrower is eligible.
  • The query accepts any positive interval, including more than seven days; student limits apply when a reservation is requested.
  • It does not know whether an item is physically on loan or present in a real catalogue.

Observe Result and State Together

  • The overlap example checks: a false result, unchanged reservations, unchanged recorded notifications.
  • An implementation that calls reserve and undoes it would also send a notice: the unchanged-notices check exposes it.
  • One that adds a temporary record directly and removes it again leaves nothing for a sequential test to see.
  • We reject that design by reading it, not by testing it.

Choose Observations by the Promise

  • For an immutable snapshot, compare the before and after lists: they expose replacement, deletion or reordering. Checking only the count misses changes to contents.
  • Inspecting a private list through reflection binds the test to representation when a public snapshot already exposes the behaviour.
  • A separate example confirms that an enquiry does not consume the next reservation identifier.

The Expected Answer Must Be Independent

  • It must come from an agreed example or rule.
  • Calculating it by calling the same method under test is circular.
  • Copying the implementation’s overlap expression into a test reproduces its mistake.
  • Concrete dates with an independently understood boundary are a better starting point.

Introduce a Runner Without Losing Earlier Evidence

  • JUnit provides annotations, assertions and reporting. Maven compiles and runs it through the configured Surefire plugin.
mvn --batch-mode verify
  • The existing scripts/check.sh now delegates to that command.
  • A LegacyRegressionTest invokes the four older main methods from four JUnit tests. A class merely placed in a test directory would not run its main.

Counts Are Not Interchangeable

  • The wrapper reports four JUnit cases.
  • Their internal output still reports: 34 Chapter 2 checks, five Chapter 3 example groups, 44 Chapter 5 checks, 28 Chapter 6 checks.
  • Do not merge those counts as if they were identical units.
  • The useful question: which behaviours execute, and what failures reach the build?

7.2 What a Test Looks Like

Arrange–Act–Assert

Arrange, Act, Assert

  • Arrange: establish the relevant state.
  • Act: perform the operation being examined.
  • Assert: inspect the outcome.
  • A reading aid, not a requirement of exactly one statement per phase.

The First Availability Test

boolean available = coordinator.isAvailable(
        "CAM1", start.plusDays(1), start.plusDays(3));

assertAll(
        () -> assertFalse(available, "An overlapping item must be unavailable"),
        () -> assertEquals(recordsBefore, coordinator.reservations()),
        () -> assertEquals(noticesBefore, inbox.received()));
  • Arranged: a two-day CAM1 reservation, with snapshots of records and notices.

A Name That Says What Broke

  • overlappingEnquiryIsUnavailableWithoutChangingRecordsOrNotifying
  • A precise name reduces the need to reverse-engineer why a setup contains particular values.
  • Avoid test1 or worksCorrectly: when the test fails, the name should tell another developer which promise was broken.

How Many Assertions?

  • Several assertions can belong together when they describe one outcome.
  • Splitting this test into three methods with identical setup may obscure the relationship between a query result and its non-mutation promise.
  • One giant method for every query, loan and notification scenario is the opposite problem: a failure is hard to locate.

Use Assertions That Can Be Wrong

  • assertTrue(true) cannot reject an implementation.
  • “No exception occurred” may be valuable for a specific promise, but it does not establish availability.
  • A meaningful assertion must distinguish the behaviour you want from a plausible defect.

Exceptional Outcomes

  • Use assertThrows for a specified exceptional outcome, and inspect state afterwards when preservation matters.
  • A catch block that prints an error and continues does not establish that the right exception occurred.
  • Wrapping a test in a broad catch can turn a failure into a false success.

Parameterised Tests

  • Small tables of policy boundaries: each row states category, count, days and expected decision.
  • The runner reports individual invocations, so a particular boundary failure is visible.
  • The table is authored from the rule, not generated by evaluating the policy itself.

The Policy Table (BorrowingPolicyTest)

CategoryCountDaysExpected
STUDENT01true
STUDENT27true
STUDENT31false
STUDENT08false
STAFF414true
STAFF51false
STAFF015false
  • Plus three more rows: a negative count and two non-positive durations. Ten rows in all.

Keep Setup Independent and Small

  • Each scenario constructs fresh mutable collaborators unless sharing is part of the behaviour.
  • A static coordinator reused across tests makes results depend on which test ran first: the same test may pass alone and fail in the suite.
  • Prefer the smallest setup that makes the scenario meaningful, using real simple domain objects.

7.3 Red, Green, Refactor

One cycle in detail

TDD in Short Cycles

  • Express an expected behaviour.
  • Observe its failure.
  • Implement enough to satisfy it.
  • Improve structure while preserving the tests.
  • A development sequence, not a label for tests added after a complete feature.

The Cycle and Its Decision Point

Write abehaviour testRun the testIntended assertionfails?Inspect setup, test orexisting behaviourImplement enoughto passConfirm greenRefactor and runregression testsYesNotests pass: next behaviourtests fail: repair the refactoring

The Starting Point for Red

  • The worked cycle begins with a compiling isAvailable that validates its arguments but always returns true.
  • This supplies an API for the first test without pretending to implement overlap detection.
mvn --batch-mode -Dtest=AvailabilityTddTest test

The Observed Red Result

  • One executed test, one failure, zero errors.
  • The assertion reports that an overlapping item was expected to be unavailable but the method returned true.
  • The unchanged-state assertions do not fail.
  • That makes the missing behaviour precise.

Not Every Failure Is Red

  • A missing library, a syntax error or an unresolved dependency is not this red result.
  • Those failures prevent or interrupt the experiment.
  • Fix the environment or compilation issue, then rerun until the intended assertion exposes the missing behaviour.

Green: Implement the Required Decision

  • Construct a LoanPeriod, inspect existing reservations, return false if the same item has an overlapping period, otherwise true.
  • It does not call reserve, allocate an identifier or invoke the sender.
  • The same test then passes: useful evidence for that example, not permission to stop thinking.

An Always-False Method Passes Too

  • An implementation that always returned false would also satisfy the first overlap test.
  • More examples are needed to distinguish overlap rejection from universal rejection:
    • empty schedule,
    • adjacency,
    • different item,
    • invalid input,
    • identifier preservation,
    • eligibility distinction.

Refactor: Share Conflict Detection

  • The green query duplicates conflict scanning already in reserve.
  • Extract a private hasConflict(itemId, requestedPeriod) used by both.
  • reserve keeps its validation and eligibility order and still stores exactly one record.
  • The query uses the same conflict meaning without acquiring reservation’s mutation or borrower-limit behaviour.

After the Extraction

  • Rerun the TDD test and the legacy bridge.
  • Observed: five JUnit cases (the new test and four legacy wrappers) pass.
  • The extraction is justified because both callers need the same meaning of a conflict, not because duplicated text is forbidden.
  • If one caller later needs stock or maintenance status, do not force that meaning into the helper.

Preserve a Traceable Sequence

  • The companion includes red, green and refactored coordinator snapshots with their commands and results.
  • In a team repository, separate commits or a recorded diff sequence give equivalent evidence.
  • Keep the actual failing assertion and correction, not just a screenshot saying tests passed.
  • A wrong implementation supplied after the fact shows test sensitivity. It does not reconstruct a test-first history that never happened.

7.4 Edges, Clocks and a Small Lending Flow

Edges that carry meaning, and time under control

Test the Edges That Carry Meaning

  • Borrowing: a student with two records may add a third; one with three may not. Seven days allowed, eight not. Staff: five records, fourteen days.
  • Overlap: an enquiry beginning at the exclusive end is available; equal start and end is invalid; a different item does not conflict because dates match.
  • Each example distinguishes a plausible error in the rule.

Boundary Selection Is Reasoning

  • It is not an exhaustive proof over every integer or date.
  • Combine examples with source inspection and, where justified, broader techniques later.
  • Do not introduce a coverage target as a substitute for an explanation of the risks.

Control Time Where Behaviour Depends on Today

  • Availability needs no clock in this version.
  • The new loan checkout does: checkout is allowed only on a date within the confirmed reservation’s half-open period.
  • LoanService receives a Java Clock and uses LocalDate.now(clock).
  • Tests use Clock.fixed, or a small controlled clock when the scenario must advance.

Instant and Zone

Instant2026-10-19 22:30 UTCZone UTClocal date: 19 OctoberZone Asia/Baghdad (UTC+3)local date: 20 OctoberLocalDate.now(clock)needs both

Why the Zone Matters

  • A fixed UTC instant does not define a local date without a zone.
  • One test uses 19 October 22:30 UTC with Asia/Baghdad and expects the local checkout date 20 October.
  • A deliberate example of zone-sensitive conversion, not reliance on the machine’s default zone.
  • Do not wait until midnight or call Thread.sleep: change the supplied clock.

A Bounded Lending Flow

  • The MVP increment adds a small LoanService linked to one coordinator in the same process.
  • It records checkout and return against a confirmed reservation.
  • A Loan is a Java record with validated identifiers, a checkout date and an optional return date. Null means one documented thing: the loan is active.
  • Records give concise value-oriented data, but validation still has to be written.

The Loan Record

public record Loan(String reservationId, String borrowerId, String itemId,
                   LocalDate borrowedOn, LocalDate returnedOn) {
    public Loan {
        if (reservationId == null || reservationId.isBlank()
                || borrowerId == null || borrowerId.isBlank()
                || itemId == null || itemId.isBlank()) {
            throw new IllegalArgumentException("Loan identifiers must not be blank.");
        }
        Objects.requireNonNull(borrowedOn, "borrowedOn");
        if (returnedOn != null && returnedOn.isBefore(borrowedOn)) {
            throw new IllegalArgumentException("Return cannot precede checkout.");
        }
    }
    public boolean active() { return returnedOn == null; }
}

LoanService.checkOut

LocalDate today = LocalDate.now(clock);
if (today.isBefore(reservation.period().start())
        || !today.isBefore(reservation.period().endExclusive())) {
    throw new IllegalStateException("Checkout must be within the reservation period.");
}
boolean itemAlreadyOut = loans.values().stream()
        .anyMatch(loan -> loan.active() && loan.itemId().equals(reservation.itemId()));
if (itemAlreadyOut) throw new IllegalStateException("Item is already on loan.");

LoanService.returnLoan

Loan current = loans.get(reservationId);
if (current == null || !current.active()) {
    throw new IllegalStateException("No active loan for this reservation.");
}
Loan returned = new Loan(current.reservationId(), current.borrowerId(),
        current.itemId(), current.borrowedOn(), LocalDate.now(clock));
loans.put(reservationId, returned);
  • The service replaces an active loan value with a returned value instead of mutating earlier snapshots.

The Lending Rules (1 of 2)

OperationRule and state effect
CheckoutReservation must exist; today’s date must be within its period
Repeat checkoutOne reservation can create at most one loan, including after return
Item already outA second active loan for the same item is rejected

The Lending Rules (2 of 2)

OperationRule and state effect
ReturnOnly an active loan can be returned; repeated return is rejected
Return dateCannot precede the recorded checkout date
ObservationLoan snapshots are unmodifiable and contain immutable loan values

Do Not Invent Lifecycle Rules Silently

  • Returning a loan does not delete or cancel its reservation.
  • The reservation-count rule still counts every stored reservation; availability stays based on those records.
  • An early return therefore does not free a reserved interval or restore a borrower’s allowance.
  • An explicit, tested inherited limitation. Changing it would disguise a feature change as testing work.

A Bounded In-Memory Flow

  • One coordinator and one loan service for the session.
  • Multiple independent loan services have independent loan maps.
  • No shared persistent or concurrent protection.
  • Not a complete lending operation.

7.5 Test Doubles

Dummies, stubs, fakes, spies and mocks

Test Doubles (1 of 2)

RolePurposeExample in this project
DummySupplies an argument the scenario should not useA sender passed to an immediately rejected request
StubProduces a controlled answer or failureA sender that throws the expected notification exception
FakeA working simplified substituteAn in-memory output standing in for external delivery

Test Doubles (2 of 2)

RolePurposeExample in this project
SpyRecords calls for later inspectionThe recorder, when a test examines received reservations
MockSupports verification of expected interactionsA Mockito sender checked for one call or no calls
  • The vocabulary follows Meszaros, summarised by Fowler. Roles overlap; the class name alone does not determine the role.

Keep the Subject Real

  • The coordinator is the subject under test in notification scenarios.
  • Mocking its reserve method would replace the behaviour we want to examine.
  • Keep the real coordinator, rules and small immutable values. Replace the output boundary that would otherwise complicate observation.
  • A hand-written recorder is often clearer than a framework: its list makes the effect visible.

Mockito for a Specific Interaction

NotificationSender sender = mock(NotificationSender.class);
ReservationCoordinator c = ReservationApplication.create(sender);
c.reserve("B1", "CAM1", day, 2);
ArgumentCaptor<Reservation> captured = ArgumentCaptor.forClass(Reservation.class);
verify(sender).send(captured.capture());
Reservation sent = captured.getValue();
assertAll(() -> assertEquals("R1", sent.id()),
        () -> assertEquals("B1", sent.borrowerId()),
        () -> assertEquals("CAM1", sent.itemId()));

Two More Mockito Tests

// no notification after a rejection
assertEquals("Rejected: borrower limit or duration.", c.reserve("B1", "CAM1", day, 8));
assertTrue(c.reservations().isEmpty());
verifyNoInteractions(sender);

// expected failure: record retained, exactly one attempt
doThrow(new NotificationException("Expected test failure"))
        .when(sender).send(any(Reservation.class));
/* ... reserve ... */
verify(sender, times(1)).send(any(Reservation.class));

Public Boundary, Not Private Helpers

  • These checks concern public boundary behaviour.
  • The exact number of invocations matters because the feature says one attempt per acceptance and no automatic retry.
  • Counting how often a private list is inspected would be a brittle assertion of a different kind.

Mockito Setup Notes

  • The supplied setup uses its interface-only proxy mock maker: mocks are Java dynamic proxies, and no class or static mocking is needed.
  • On newer JDKs, Mockito may still attach its ByteBuddy agent and the JVM prints “A Java agent has been loaded dynamically”. The tests still pass.
  • Passing -XX:+EnableDynamicAgentLoading to the test JVM removed the message. The JVM says dynamic loading will be disallowed by default in a future release: check Mockito’s documentation for your JDK.

A Preview: Doubles for an Outside Data Source

BorrowerDirectory knownStudent = id -> Optional.of(new Borrower(id, BorrowerCategory.STUDENT));
BorrowerDirectory nobody = id -> Optional.empty();
BorrowerDirectory offline = id -> { throw new DirectoryException("provider offline"); };
  • The contract has one method, so a lambda is enough. Chapter 8 builds the real types.
  • Compiled and run against Chapter 8’s types, they print STUDENT, false and failure: provider offline.

What Each Directory Double Answers

  • knownStudent: a stub with a fixed answer: tests what the service does with a borrower it can trust.
  • nobody: a stub saying “absent or inactive”: tests the ordinary rejection.
  • offline: a throwing stub: tests that an outage is reported as an outage, not as “not found”.
  • None needs a framework. None proves that a real provider behaves this way.

Match the Double to the Question

  • A recorder when you want to inspect accumulated notices.
  • A throwing stub when you need a predictable output failure.
  • An interaction-verifying mock when the contractual call itself is the observation.
  • A fixed clock supplies controlled time; it does not prove the real system clock is correct.
  • Neither recorder nor mock proves that email is delivered. Chapter 13 covers real adapters.

Do Not Verify Everything

  • A blanket “nothing else was called” assertion can make harmless implementation changes fail.
  • Verify only absence or ordering that is meaningful to the requirement.
  • “No notification after rejection” is meaningful.
  • “Exactly three calls to the snapshot getter” is not.

7.6 What Testing Tells You About Design

Difficult tests as information

Difficult Setup Can Reveal a Design Problem

  • Chapter 6’s constructor injection lets tests choose a recorder, failing sender and policy mapping without altering the coordinator.
  • LoanService receives a clock for the same reason.
  • A difficult test may expose hidden global configuration, mixed responsibilities or unclear results. It may also expose a poorly chosen test: is the scenario trying to inspect a private detail?

The Query’s Clear Contract

  • The availability query creates no records and has no notification side effects: easy to test result and state separately.
  • Implementing it by calling reserve would entangle eligibility, identifiers and notifications.
  • Test difficulty would then be useful feedback about the proposed design.

Use Real Simple Collaborators

  • LoanPeriod is cheap, deterministic and central to the rule. Mocking it to say “overlaps” would prevent the tests from exercising real period semantics.
  • An ordinary list or map should usually remain real: replacing it with a mock leads to assertions about add, get and ordering, not preserved records.
  • A substitute should remove an obstacle or expose a meaningful boundary, not erase the behaviour under examination.

Preserve Evidence During Migration

  • Moving to JUnit does not require rewriting every old assertion at once.
  • The bridge makes earlier runners visible to the new build; new tests are focused methods with readable names.
  • The bridge is transitional: four coarse results give less precise reporting. Later migration can improve diagnosis without changing expected behaviour.
  • Do not discard earlier checks because their syntax predates the framework.

7.7 Tests That Look Good but Are Weak or Brittle

Executing code is not asking a question

Coverage Is Not Correctness

  • Coverage records which code was exercised under a particular measurement.
  • It does not establish that the expected answer was correct or that the assertions would reject a defect.
  • A test can execute every line of a method and accept every result.

Compare Two Query Tests

  • One only calls isAvailable.
  • The other expects an overlapping interval to be false and preserves the recorded state: it distinguishes a meaningful defect.
  • A further adjacency example distinguishes a different defect the overlap example would miss.

A Targeted Mutation Experiment

  • In a separate copy, change the second comparison in LoanPeriod.overlaps:
other.start.isBefore(endExclusive)        // correct
!other.start.isAfter(endExclusive)        // deliberately wrong
mvn --batch-mode -Dtest=AvailabilityTest#adjacentPeriodAndDifferentItemAreAvailable test

The Result

  • One failure, zero errors, nonzero build exit. The assertion expects true and receives false.
  • A behavioural failure in the adjacency example, not a missing tool or compile issue.
  • Restore the original and rerun: the test passes.
  • It supports one claim: this test detects this fault. It is not a mutation score.
  • Never change the correct expected value to accommodate the mutant.

Sixteen Single Faults

  • Introduced one at a time into copies of the completed project; each run through mvn test.
  • Fifteen were rejected by at least one named test.
  • One survived.
  • A surviving fault is information to examine, not always a defect in the tests.

Which Test Caught Which Fault (1 of 4)

FaultCaught by
isAvailable always returns trueAvailabilityTddTest overlapping enquiry
isAvailable always returns falseAvailabilityTest empty-schedule, adjacency, eligibility examples
Conflict check ignores the item identifieradjacency/different-item example and the Chapter 2 bridge
Inclusive end boundary in LoanPeriodadjacency test and both older bridges

Which Test Caught Which Fault (2 of 4)

FaultCaught by
Checkout allowed on the exclusive end daycheckoutOutsideHalfOpenPeriodIsRejected
Checkout allowed before the startthe same test
One reservation checked out twiceaReservationCannotBeCheckedOutTwiceEvenAfterReturn
Item-already-on-loan check removedunreturnedItemBlocksTheNextAdjacentReservationUntilReturned

Which Test Caught Which Fault (3 of 4)

FaultCaught by
Return of an inactive loan allowedcheckoutAndReturnUseTheSuppliedClock
Real system clock used instead of the supplied onethree LoanServiceTest methods
Clock zone ignoredclockZoneDeterminesTheCheckoutDate
Return date recorded as the checkout datecheckoutAndReturnUseTheSuppliedClock and the backward-clock test

Which Test Caught Which Fault (4 of 4)

FaultCaught by
Loan no longer rejects a return before checkoutbackwardClockCannotRecordReturnBeforeCheckout
Sender called twice on successNotificationMockitoTest, Chapter06Checks, an availability test

A Real Gap Found

  • The rule table says loan snapshots are unmodifiable, but no test checked it.
  • Replacing List.copyOf(loans.values()) with a plain ArrayList passed the other tests.
  • One assertion was added (in checkoutAndReturnUseTheSuppliedClock), which now rejects that fault.

The Mutant That Survives

  • An enquiry that adds a temporary record and removes it again.
  • With identifiers derived from the list size, it leaves no trace: records, notices and next identifier end exactly as before.
  • In a sequential program, no test can tell it from the correct one. Only a concurrent observer could.
  • Response: reject the design by reading it; do not claim a test can catch it. The identifier example is still worth keeping.

Do Not Let Tests Reproduce the Same Misunderstanding

  • An AI-generated implementation and AI-generated tests may agree because both assume inclusive end dates. Agreement is not independent evidence.
  • Check expected values against the human-reviewed convention and concrete examples.
  • Avoid expected values derived from the result: an identifier equal to the identifier in the same returned object is no independent fact. For the first reservation, an explicit R1 is meaningful.

Avoid Unnecessary Implementation Coupling

  • A test asserting the query calls private hasConflict exactly once fails under a different correct structure.
  • The public result and state contract should survive refactoring.
  • Exact text assertions are fine when text is part of the existing contract, but wording changes then require deliberate expectation updates.

When a Test Fails, Classify the Failure

  • A requirement misunderstanding.
  • An implementation defect.
  • An outdated expectation.
  • An environment or configuration problem.
  • Test fragility.
  • A green build obtained by suppressing the assertion’s exit status answers none of these.

7.8 Preparing the Milestone 2 Evidence

Demonstrate an outcome with its boundaries

The Demonstration Journey

Available before reservation: true
Confirmed: R1 for B1 on CAM1 [2026-10-20, 2026-10-22).
Available after reservation: false
Loan active after checkout: true
Loan active after return: false
Accepted-reservation notices: 1
  • Fixed clock; one coordinator and loan service. Checkout and return send no new notices.

What the Demonstration Is and Is Not

  • A concrete example of a bounded MVP increment. Your assessed domain and scope may differ.
  • No remote delivery, database or hosted execution.
  • Another person should be able to reproduce it and explain what remains outside its claim.
  • A hosted CI run is separate evidence from the local Maven runs.

Milestone 2 Evidence (1 of 3)

EvidenceWhat it should establish
Working MVP demonstrationA coherent user outcome runs from documented setup
Acceptance mappingSelected examples connect to executed tests
TDD sequenceIntended failure, implementation and protected refactoring are visible

Milestone 2 Evidence (2 of 3)

EvidenceWhat it should establish
OO/SOLID rationaleCollaborators and responsibilities have concrete reasons
Double choiceControlled behaviour or interaction is justified
Build resultActual tests ran against the intended revision

Milestone 2 Evidence (3 of 3)

EvidenceWhat it should establish
Review and retrospectiveReal feedback, a change in understanding and a next action are recorded
LimitationsIn-memory storage, lifecycle boundaries and missing integrations are explicit
  • A test report is not the whole milestone.

Make the Retrospective Useful

  • Connect an observation to an action.
  • Example (record it only if it happened): availability tests silently assumed the machine’s current date, so all time dependencies become explicit.
  • Avoid pre-filled success narratives: “we collaborated well” explains nothing.
  • Distinguish a proposed improvement from one already tested.

7.9 Keeping Implementation and Expected Behaviour Apart, with AI

Do not let outputs validate each other

Use the Reviewed Examples as Authority

  • Ask an AI assistant for a candidate query implementation.
  • Separately ask for omitted boundary cases.
  • Evaluate both outputs rather than letting them validate each other.
  • Chapter 15 collects the recurring AI workflows. This activity is self-contained.

Ground Rules for the Activity (1 of 2)

For this activity
Your goalSeparate a proposed implementation from the evidence that would accept or reject it
You start withAQ-01 to AQ-05 as reviewed, and Chapter 6’s coordinator
What is in boundsEvaluate the candidate implementation and its tests; do not treat either as proof of the other

Ground Rules for the Activity (2 of 2)

For this activity
How to checkThe reviewed examples, the labelled wrong-boundary mutation, the build result
How to decideA verdict with independent evidence for each claim
No AI account?The ready-made example is enough

The Ready-Made Example

Author-written fictional AI output.

An enquiry can call reserve and remove the temporary record. Treat the end date as inclusive so adjacent bookings conflict. A test is sufficient if the method returns without throwing. Since the implementation and generated tests agree, coverage will prove correctness. For time-sensitive loan tests, use the current date and wait until tomorrow to test the next state.

Model Critique (1 of 2)

ClaimVerdictIndependent check or correction
Query by reserve-and-removeRejectreserve sends a notice and can be refused by limits; adding and removing directly leaves no trace sequential tests can see, so reject by reading
Inclusive end means adjacency conflictsRejectThe agreed interval is half-open; [20,22) and [22,23) do not overlap
No exception is sufficientRejectAn always-true or always-false method can return normally and be wrong

Model Critique (2 of 2)

ClaimVerdictIndependent check or correction
Implementation and tests mutually prove correctnessRejectBoth may share the same mistaken convention
Coverage proves correctnessRejectExecution does not validate expected values or sensitivity to defects
Use the real date and wait for tomorrowRejectSupply a fixed or controlled clock and an explicit zone
  • The wrong-boundary experiment is a concrete verification step: no need to ask another AI whether the mutant is wrong.

7.10 Testing Habits That Mislead

Eight to avoid

Mistakes 1 to 3

MistakeConsequenceBetter action
Count a dependency-download error as TDD redBehaviour was never examinedReach the intended assertion failure
Write expected values by calling the implementationReproduces the same mistakeUse independently reviewed examples
Mock the object under testReplaces the behaviour being investigatedKeep the subject real

Mistakes 4 to 6

MistakeConsequenceBetter action
Use real time for a deterministic date ruleResults change with date or zoneInject an explicit clock
Treat every recorder as the same double roleHides why it is usedDescribe its role in the particular test
Verify private call countsBreaks harmless refactoringObserve public behaviour or meaningful boundary interactions

Mistakes 7 and 8

MistakeConsequenceBetter action
Remove old checks during tool migrationLoses existing evidenceBridge or migrate them explicitly
Call the bounded demo production-readyConceals lifecycle and integration gapsState what the MVP actually demonstrates

7.11 Try It Yourself

A test-first change and Milestone 2 evidence

What You Start With, and the Demonstration

  • You start with: Chapter 6’s version, the reviewed query criteria, the new loan-flow rules and the companion’s Maven/JUnit setup.
  • Demonstration: run the labelled red stage, inspect the precise failure, apply the green implementation, extract conflict detection, rerun the new test and the legacy bridge.
  • Guided practice: convert one policy boundary table into parameterised examples and discuss which incorrect result each row distinguishes. Compare a hand-written recorder with the Mockito interaction test.

Independent Work: Steps 1 to 3

  1. Reproduce the supplied TDD cycle, or conduct a small new one. Record the actual sequence.
  2. Complete the remaining query examples, including side effects and adjacency.
  3. Demonstrate the loan flow with controlled dates and rejection of an invalid return.

Independent Work: Steps 4 to 6

  1. Choose a substitute for one collaborator and justify its role.
  2. Run the labelled wrong-boundary implementation in a separate copy, identify the failing assertion and restore the source.
  3. Run the full build and map selected acceptance examples to named tests.

Independent Work: Steps 7 to 9

  1. Rehearse the MVP demonstration from documented instructions, obtain actual review feedback and write a short retrospective with a next action.
  2. On your team’s project, take one backlog behaviour and develop it test-first. Record the actual red failure, the green change and the refactoring. Do not reconstruct a sequence afterwards.
  3. On your team’s project, work through the Milestone 2 checklist: rehearse your own demo, get review feedback, write a retrospective. Record which items are complete, which are gaps and who will close each one.

Expected Output and Completion

  • Output: runnable source; focused tests; TDD and mutation evidence; double rationale; actual build result; demonstration notes; review and retrospective notes; explicit limitations; the step 8 and 9 records.
  • Complete when: the failure is behavioural and understood; query results and state effects match the examples; time-sensitive tests do not depend on the actual day; the whole regression suite executes; the demo includes reservation, checkout and return.
  • If you want more: sketch how completed reservations should affect counts and early-return availability. Do not silently implement it.

What the Build Establishes

  • 35 JUnit invocations: one TDD test, eight additional availability invocations, ten policy rows, nine loan invocations, three Mockito tests, four legacy wrappers.
  • The expected output ends Tests run: 35, Failures: 0, Errors: 0, Skipped: 0 and BUILD SUCCESS.
  • An execution inventory, not a claim of exhaustive correctness or a quality score.

Versions Pinned for This Chapter

ToolTested selectionRole
JavaJava 17 language/API targetApplication and test execution
Maven3.9.11Build and dependency resolution
Compiler Plugin3.14.1Java compilation
Surefire Plugin3.5.4JUnit Platform execution
JUnit Jupiter5.13.4Assertions, discovery, parameterised cases
Mockito5.20.0Interface mock, stubbing, verification

Acceptance Mapping (1 of 2)

Requirement or riskExecutable evidence
AQ-01 empty schedule availableemptyScheduleIsAvailableWithoutAnySideEffect
AQ-02 overlap unavailableAvailabilityTddTest overlapping enquiry
AQ-03 adjacent periods allowedadjacentPeriodAndDifferentItemAreAvailable
AQ-04 different itemthe same test checks MIC1 separately
AQ-05 equal or reversed intervalparameterised invalidPeriodFailsWithoutChangingState

Acceptance Mapping (2 of 2)

Requirement or riskExecutable evidence
Enquiry does not allocate an IDqueryDoesNotAllocateAnIdentifier
Availability differs from eligibilitytwo tests: count limit and a thirty-day enquiry
Checkout window excludes its endtwo invocations of checkoutOutsideHalfOpenPeriodIsRejected
Local date depends on clock zoneclockZoneDeterminesTheCheckoutDate
Backward time cannot predate checkoutbackwardClockCannotRecordReturnBeforeCheckout

Suggested Evidence Packet (1 of 2)

DocumentUseful content
Scope noteSelected query and loan rules; synthetic inputs; lifecycle limitations
TDD recordActual red message, same test at green, refactoring diff, regression result
Acceptance mappingNamed tests tied to concrete examples
Double rationaleWhich collaborator is replaced, why, and what observation it enables

Suggested Evidence Packet (2 of 2)

DocumentUseful content
Mutation recordIsolated faulty comparison, selected failure and restored pass
Build resultExecuted revision, tool versions, actual report
DemonstrationReproducible steps from empty state through reserve, checkout, return
Review and retrospectiveActual observations, corrective action, a next-iteration experiment
  • A copied sample table is not evidence that a team ran the tests or conducted a review.

7.12 Questions to Think with, and Midterm Practice

Concept, diagnosis, transfer, and the practice set

Concept Questions

  1. Why is “the method did not throw” insufficient evidence for an availability query?
  2. What distinguishes the red stage of the worked cycle from a missing-dependency failure?
  3. When does the notification recorder act as a spy, and why can a framework mock serve a similar purpose?
  4. Why do both the instant and zone matter when testing a local-date checkout rule?

Diagnosis Questions

  1. A test calculates its expected availability by calling the same overlap helper used by production code. Both treat adjacency as overlap. Explain why the green result is weak and replace it with an independent example.
  2. A loan test passes alone but fails after another test, and both use a static shared loan service. Diagnose the likely cause and propose a setup strategy that still allows intentional multi-step scenarios.

Guidance: Questions 1 and 2

  1. A valid query must distinguish available from conflicting intervals and preserve state. Always-true returns normally but fails AQ-02; always-false fails AQ-01 and AQ-03.
  2. The worked red run discovered and executed the named test, reached the assertion and reported false expected but true actual. A dependency-resolution failure never establishes application behaviour.

Guidance: Questions 3 and 4

  1. When the test examines its received records, the recorder provides observed interaction history: a spy. A Mockito mock can record and verify too. The classification depends on the test’s purpose and observation.
  2. One instant gives different dates in different zones. The test must state both when the rule concerns a local date. The Baghdad example fixes the interpretation.

Guidance: Questions 5 and 6

  1. The test derives its expectation from the same faulty rule, so the two agree without independent validation. Book [2026-10-20, 2026-10-22), enquire [2026-10-22, 2026-10-23), explicitly expect true, and run the mutant to show this rejects the inclusive comparison.
  2. Earlier tests may have inserted or returned loans that alter later tests’ starting state. Use a fresh coordinator, sender and loan service per independent scenario. A multi-step lifecycle test may share its own service while advancing a controlled clock.

Transfer Challenge: Room Booking

A room-booking service permits availability enquiry and check-in only during the booked interval.

  • Design three acceptance examples, one clock-controlled test and one collaborator substitute.
  • Explain which expectations transfer from equipment lending and which rules need stakeholder clarification, such as cleaning time or late check-in.

Transfer Guidance

  • Examples: an empty schedule is available; an overlapping booking makes an enquiry unavailable without adding a booking; an enquiry starting at the previous exclusive end is available if no turnaround rule applies.
  • Check-in: fixed clock at the booking start: accept; at the exclusive end: reject if that is the agreed policy. A recorder observes one check-in notice.
  • Clarify cleaning intervals, early arrival, late check-in, no-show expiry, who authorises entry.
  • Represent the time zone and choose an appropriate time type. Do not copy LocalDate merely because the earlier project used whole days.

Midterm Practice Set: What It Covers

  • The midterm covers Chapters 2 to 7 (Weeks 1 to 6).
  • According to the module descriptor, it asks you to:
    • model a scenario with UML class diagrams,
    • identify SOLID violations and smells in supplied code,
    • refactor short snippets by introducing interfaces and dependency injection,
    • write unit tests with test doubles for a small component.
  • Do not add Chapter 8’s patterns. Whether Mockito is allowed in the exam is the module leader’s decision; hand-written doubles work anywhere.

Task 1: UML

Draw a class diagram. Include multiplicities, one composition, one interface realisation and one dependency, and write down one rule your diagram cannot express.

  • A library lends laptops. Each laptop has an asset tag and a model.
  • A loan links one borrower to one laptop and records a start date and a due date.
  • A borrower and a laptop can each appear in many loans over time.
  • A loan can be renewed any number of times; a renewal records a new due date and has no meaning without its loan.
  • When a loan is overdue, a notice is delivered through a channel: email today, other channels later.

Task 1: One Model

Borrower-String nameLaptop-String assetTag-String modelLoan-LocalDate startedOn-LocalDate dueOn+notify(NoticeChannelchannel) voidRenewal-LocalDate newDueOn«interface»NoticeChannel+deliver(Stringmessage) voidEmailChannel10..*borrows10..*is lent in10..*hasnoticesimplements

Task 1: Reading the Model

  • A borrower and a laptop each link to any number of loans over time: 0..* at the loan side.
  • A renewal cannot exist without its loan: composition.
  • The channel is an interface because other channels are expected; Loan depends on the interface, not on EmailChannel.
  • A rule such as “a laptop is on at most one active loan” cannot be expressed by multiplicity: write it as a note or invariant.
  • Penalise inheritance for “has”, a solid line for realisation, a composition diamond between Borrower and Loan.

Task 2: Diagnose This Class

public class OverdueReminder {
    public boolean remind(String borrowerEmail, LocalDate dueDate, String itemName) {
        if (LocalDate.now().isAfter(dueDate)) {
            MailClient client = new MailClient("smtp.campus.example", 25);
            client.send(borrowerEmail, "Overdue: " + itemName,
                        "Please return " + itemName + ".");
            System.out.println("Reminder sent to " + borrowerEmail);
            return true;
        }
        return false;
    }
}
  • MailClient opens a real network connection: assume you cannot call it from a test.

Task 2: A Strong Diagnosis

  • Three jobs mixed: deciding overdue, building and sending the message, reporting to the console (single responsibility).
  • Reads the system clock directly: the result depends on the date the test runs.
  • Constructs MailClient with a hard-coded host and port: a concrete provider it cannot swap (dependency inversion), and untestable without a mail server.
  • The boolean does not distinguish “not overdue” from “sent”.
  • Connect each defect to a consequence: “the overdue path cannot run in a test; here it throws IllegalStateException from MailClient”.

Task 3: Refactor It

public interface ReminderSender {
    void send(String to, String subject, String body);
}

public final class OverdueReminder {
    private final Clock clock;
    private final ReminderSender sender;

    public OverdueReminder(Clock clock, ReminderSender sender) { /* null checks */ }

    public boolean remind(String borrowerEmail, LocalDate dueDate, String itemName) {
        if (!LocalDate.now(clock).isAfter(dueDate)) return false;
        sender.send(borrowerEmail, "Overdue: " + itemName, "Please return " + itemName + ".");
        return true;
    }
}

Task 3: What Was Preserved

  • A MailReminderSender adapts the supplied MailClient and keeps the console message, so behaviour is preserved.
  • The composition root would call new OverdueReminder(Clock.systemDefaultZone(), new MailReminderSender(new MailClient("smtp.campus.example", 25))).
  • The constructor is a deliberate API change; the observable behaviour of remind is preserved.
  • No credit for adding an interface to MailClient (you do not own it) or for a factory nobody needs.

Task 4: Tests With a Double

@Test void noReminderOnTheDueDateItself() {
    assertFalse(reminderOn("2026-10-20T23:00:00Z").remind("[email protected]", DUE, "CAM1"));
    assertTrue(sender.calls.isEmpty());
}
@Test void exactlyOneReminderTheDayAfterTheDueDate() {
    assertTrue(reminderOn("2026-10-21T00:00:00Z").remind("[email protected]", DUE, "CAM1"));
    assertEquals(List.of("[email protected]|Overdue: CAM1|Please return CAM1."), sender.calls);
}
  • A hand-written spy records calls; the clock is fixed; expected values come from the scenario.
  • Also test an earlier date. State which role your double plays.

Task 5: Trace a Rejected Reservation

  • Trace, in order: argument validation, borrower count, policy decision, conflict checking.
  • A rejection occurs before storage and notification.
  • Choose observations that show it: the rejection outcome, unchanged records, no sender interaction.
  • Identify the role of the injected sender.
  • A good answer connects design, behaviour and evidence rather than listing acronyms.

What to Take Away, and What Comes Next

What to carry forward

Summary

  • Meaningful tests distinguish intended behaviour from plausible mistakes.
  • The worked cycle makes one missing rule fail visibly, implements it and protects the following structural change.
  • Controlled collaborators and clocks make outcomes reproducible; carefully chosen assertions keep tests useful during refactoring.

Where the Project Stands

  • Reservation availability enquiry.
  • A bounded checkout and return flow.
  • Lifecycle and integration limits remain explicit.

Next: Chapter 8

  • Construction and compatibility patterns.
  • They build on these tests, rather than replacing design judgement with pattern names.

Sources and Further Reading

  1. JUnit Team: JUnit 5 User Guide, 5.13.4.
  2. Apache Maven: Surefire with the JUnit Platform; Maven 3.9.11; Compiler Plugin.
  3. Fowler: Test Driven Development; Test Double (2006).
  4. Oracle: Java SE 17 Clock; Record Classes.
  5. Mockito Team: API documentation; Mockito 5.20.0.
  6. Martin: Agile Software Development (2002). Beck: Test-Driven Development: By Example (2002).
  • Full references and links are in the chapter text.