Skip to content

Lab 04 DefectsΒ #69

Description

@antonyboom

Lab 04 (Spring Boot / Java Track) β€” Defect Report

Lab: lab-04-testing-documentation-workflow-java.md
Track affected: 🟩 Spring Boot / Java only (.NET, Kotlin and Swift versions are separate files)
Date: 2026-09-21
Status: Open β€” lab completed with workarounds, lab document not yet corrected
Related: LAB_01_SPRINGBOOT_DEFECTS.md Β· LAB_02_SPRINGBOOT_DEFECTS.md Β· LAB_03_SPRINGBOOT_DEFECTS.md


Summary

Lab 04 is the shortest lab and has the highest proportion of fabricated content. Every worked
example in Parts 1 and 2 targets a class that does not exist, using construction idioms the real
types cannot support. Part 3 stages directories that were never created, and Part 4's sample PR
description asserts facts about the codebase that are false β€” including "Breaking Changes: None"
for work that introduces three.

The underlying activities β€” coverage, documentation, commits, PR prep β€” are all worthwhile, and
were completed. Almost none of the supplied material could be used.

# Severity Defect Participant impact
1 πŸ”΄ Blocking /tests and /doc target a non-existent class No file to open; both parts have no starting point
2 πŸ”΄ Blocking Samples use builders and setters that cannot exist Nothing compiles; records are immutable by design
3 πŸ”΄ Blocking TaskStatus.TODO / DONE again Every status assertion fails
4 🟑 Moderate ./mvnw and ./gradlew β€” neither wrapper exists Commands fail immediately
5 🟑 Moderate Wrong packages throughout Imports resolve to nothing
6 🟑 Moderate Part 3 stages a CQRS layer that was never built git add matches nothing
7 🟑 Moderate README sample documents a different API Publishes wrong docs
8 🟑 Moderate PR sample states false facts about the codebase Teaches fabrication as a workflow
9 🟒 Minor Invalid Mockito in a sample test Throws at runtime if copied
10 🟒 Minor Asks to create an ADR that already exists Confusing, would overwrite

Defect 1 β€” /tests and /doc target a non-existent class

Severity: πŸ”΄ Blocking

Where

Part 1.1 Step 1 and Part 2.1 Step 1.

What

Open src-springboot/taskmanager-application/src/main/java/com/example/taskmanager/application/service/CreateTaskService.java and select the createTask method.

There is no CreateTaskService. There is no application/service/ directory either β€” the real
package is application/services/ (plural), and it contains TaskService, a single concrete
class holding all task use cases.

Why it matters

Both Part 1 and Part 2 open with this instruction. A participant cannot begin either part: there
is no file to select, and therefore nothing for /tests or /doc to act on.

Proposed fix

Point at TaskService and pick a concrete method β€” createTask(String, String, Priority, LocalDateTime) is a good /tests target, and the class-level Javadoc is a good /doc target.

Workaround applied

Ignored the named class. Used JaCoCo to find real gaps instead, which is a better exercise
than /tests on an arbitrary method β€” see "What replaced Part 1" below.


Defect 2 β€” Samples use builders and setters that cannot exist

Severity: πŸ”΄ Blocking

Where

Part 1.1 Step 3 and Part 1.2, throughout the generated test samples.

What

Lab sample Reality
Task.builder()...build() Task has a private constructor and static factory methods; no builder
CreateTaskRequest.builder() CreateTaskRequest is a record; no builder
validRequest.setTitle(invalidTitle) Records are immutable; no setters
taskResponse.getTitle() Record accessor is title()
request.getTitle() in the Javadoc sample Same
Task.builder().id(...).status(TaskStatus.TODO) Status is assigned by the aggregate, never set externally

Why it matters

This is not a naming mismatch that a rename fixes. The samples assume a mutable, builder-based
anemic model; the codebase deliberately uses immutable records and an aggregate that protects its
own invariants. Following the lab means undoing three labs' worth of design.

The setter samples are the clearest signal: validRequest.setTitle(invalidTitle) mutates a shared
fixture across parameterized runs, which is exactly the pattern the record-based design prevents.

Proposed fix

Regenerate the samples against the real API: Task.create(title, description, priority, dueDate),
new CreateTaskRequest(title, description, priority, dueDate), and per-case construction instead
of fixture mutation.


Defect 3 β€” TaskStatus.TODO / DONE again

Severity: πŸ”΄ Blocking

Where

Part 1.1 (TaskStatus.TODO in three places), Part 1.2 (jsonPath("$.status").value("TODO")),
Part 2.2 (README sample lists TODO, IN_PROGRESS, DONE, CANCELLED).

What

The enum is PENDING, IN_PROGRESS, COMPLETED, CANCELLED.

Why it matters

Identical to Lab 03 Defect 3, now propagated into documentation the lab tells participants to
publish. A reader of the resulting README would be told the API accepts values it rejects.

Confirmed against the running application:

GET /api/tasks?status=DONE
400 : Invalid status: DONE. Valid values: [PENDING, IN_PROGRESS, COMPLETED, CANCELLED]

Proposed fix

Correct in all five locations. Given this is the second lab carrying the same error, the value
list is worth extracting into a single shared snippet.


Defect 4 β€” ./mvnw and ./gradlew β€” neither wrapper exists

Severity: 🟑 Moderate

Where

Part 1.3 and the PR sample's "Testing Performed" section.

What

./mvnw test

Or if using Gradle:

./gradlew test

src-springboot/ contains no Maven wrapper (mvnw, mvnw.cmd, .mvn/) and is not a Gradle
project. Every other lab uses plain mvn.

Why it matters

The command fails instantly with "command not found". Offering a Gradle alternative also implies
a choice that does not exist β€” the Spring Boot track is Maven-only.

Proposed fix

Use mvn clean test, matching Labs 1–3. Drop the Gradle alternative, or add a genuine wrapper via
mvn wrapper:wrapper if wrapper usage is the intended lesson.


Defect 5 β€” Wrong packages throughout

Severity: 🟑 Moderate

Where

Every import block in Parts 1 and 2.

What

Lab Reality
com.example.taskmanager.domain.entity ...domain.tasks
com.example.taskmanager.domain.repository ...domain.tasks
com.example.taskmanager.domain.valueobject ...domain.tasks
com.example.taskmanager.application.dto ...api.dto
com.example.taskmanager.application.service ...application.services

Why it matters

The domain module is feature-oriented β€” TaskId, TaskStatus, Priority, Task and
TaskRepository all live in domain.tasks, as the repository instructions require. The lab
assumes technical grouping, which is the layout those instructions explicitly discourage.

Note this also contradicts Lab 02, which placed DTOs in application.dto β€” neither matches the
repository, and they do not match each other.


Defect 6 β€” Part 3 stages a CQRS layer that was never built

Severity: 🟑 Moderate

Where

Part 3.1 and the Part 3.2 sample commit message.

What

git add src-springboot/taskmanager-application/src/main/java/com/example/taskmanager/application/query/

There is no query/ package. The sample commit message then describes work nobody did:

  • Implement GetTasksQuery and GetTaskByIdQuery handlers

No such classes exist. The repository has a single concrete TaskService, and
springboot.instructions.md explicitly says "no CQRS libraries for this workshop".

The second sample, for LegacyTaskProcessor, is fabricated in a subtler way:

  • Remove nested for loops and replace with Stream API
    Reduces cyclomatic complexity from 15 to 4 per method.

The real refactor uses a StringBuilder loop, not the Stream API β€” a per-character transformation
is one of the few places a plain loop genuinely reads better. And the complexity figures are
invented; nothing measured them.

Why it matters

git add on a non-existent path silently matches nothing, so a participant commits an empty
staging area or, worse, does not notice. More seriously, the lab is teaching people to write
commit messages describing work they did not do, with metrics they did not measure. That is the
opposite of the lesson.

Proposed fix

Stage real paths. Replace the fabricated bullets with a note that specifics and metrics must come
from the actual diff β€” and that inventing them is the failure mode to avoid when an assistant
drafts your commit message.

Workaround applied

Wrote Conventional Commits describing the real changes, with ! markers and BREAKING CHANGE:
footers for all three breaking changes.


Defect 7 β€” README sample documents a different API

Severity: 🟑 Moderate

Where

Part 2.2, "Generate API Documentation (README)".

What

The sample README section documents:

  • GET /api/tasks?completed=true β€” there is no completed filter; filters are priority,
    status, sort, direction, page, size
  • A bare array response β€” GET /api/tasks returns a paged envelope
  • PUT /api/tasks/{id} β€” the real update verb is PATCH
  • Status values TODO / DONE β€” see Defect 3
  • No mention of GET /api/tasks/due-soon
  • A create example without priority, which now returns 400

Why it matters

Part 2 instructs participants to add this to README.md. Following it publishes documentation
that is wrong in six ways for an API sitting in the same repository.

There is a related trap: src-springboot/README.md already had an API Endpoints section, so
the instruction to "create an API documentation section" produces a duplicate rather than
updating the stale one.

Workaround applied

Rewrote the existing section in src-springboot/README.md to match reality β€” all nine endpoints,
the query-parameter table, the paged envelope, real field values, the required priority, and the
RFC 7807 error shapes.


Defect 8 β€” PR sample states false facts about the codebase

Severity: 🟑 Moderate

Where

Part 4.1, "Expected Output".

What

Sample claim Reality
"CQRS pattern for commands and queries" No CQRS; instructions forbid it
"Unit Tests: 45+ tests" 305
"Coverage: ~92% code coverage" Never measured before this lab; nothing produced that figure
"Average response time: <50ms", "No memory leaks detected in load testing" No load testing was performed
"Breaking Changes: ⚠️ None" Three, all in the work these labs produce
"Migration Required: None β€” Uses in-memory H2" Flyway migrations are required; production runs ddl-auto: validate
"Stream API instead of nested loops" StringBuilder loop

Why it matters

This is the most damaging defect in the lab, because it is the template participants are told to
reuse. It models inventing test counts, coverage percentages and performance figures, and β€” worst
β€” asserting "no breaking changes" on work whose headline change is that POST /api/tasks now
rejects previously valid requests.

A reviewer trusting that line would approve a contract break.

Proposed fix

Replace with a PR description drawn from the actual diff, and add an explicit warning: an
assistant will happily invent coverage numbers and performance claims, so every factual assertion
in a generated PR description must be verified before posting.

Workaround applied

Wrote a PR description with real counts, a prominent ⚠️ Breaking changes section listing all
three, a Known Gaps section, and a reviewer checklist that asks whether the breaks are acceptable.


Defect 9 β€” Invalid Mockito in a sample test

Severity: 🟒 Minor

Where

Part 1.1, createTask_LogsInformationMessages.

What

when(taskRepository.save(any(Task.class))).thenReturn(any(Task.class));

An argument matcher is used as a return value. Mockito throws
InvalidUseOfMatchersException at runtime.

The neighbouring createTask_SetsCorrectDefaultStatus also declares
Task capturedTask = Task.builder().build(); and never uses it.

Why it matters

Minor, but it is in a sample presented as Copilot's "comprehensive" output, and it fails in a way
that is confusing to diagnose β€” the error surfaces on an unrelated subsequent stubbing.

Proposed fix

thenAnswer(invocation -> invocation.getArgument(0)), and delete the unused variable.


Defect 10 β€” Asks to create an ADR that already exists

Severity: 🟒 Minor

Where

Part 2.3, "Generate Architecture Documentation (ADR)".

What

Create an Architecture Decision Record (ADR) in docs/adr/0001-use-clean-architecture.md

0001-use-clean-architecture.md already exists and is
referenced by the architecture docs.

Proposed fix

Ask for an ADR covering a decision the participant actually made during the labs, at the next free
number. That is a genuinely better exercise: the decision is fresh and the tradeoffs are real.

Workaround applied

Wrote ADR 0003 on paging in the
application layer, following ADR 0002 from Lab 02.


Verification status of the reference implementation

cd src-springboot && mvn clean test β†’ BUILD SUCCESS

Module Tests Before Lab 4 After
taskmanager-domain 76 65 +11
taskmanager-application 123 106 +17
taskmanager-infrastructure 52 48 +4
taskmanager-api 54 54 β€”
Total 305 273 +32

What replaced Part 1

Rather than running /tests on an arbitrary method, JaCoCo was used to find classes with genuinely
untested logic. Four stood out, and all four are now fully covered:

Class Before After Gap that was hiding there
TaskSortField 0% 100% Comparators were only exercised indirectly through API tests
TaskProcessingException 0% 100% Never constructed β€” the failure path was untested
FileTaskOutputWriter 69% 100% The IO failure path was untested
TaskId 50% 100% TaskId.of(String) and its malformed-input rejection

The TaskSortField gap is the instructive one: the class had zero unit coverage despite its
behaviour being asserted by API integration tests. Per-module coverage made that visible, and the
nulls-last ordering rule β€” the subtlest logic in the class β€” now has direct tests in both
directions.

This is a better exercise than the lab's, and worth adopting: measure first, then write the
tests the measurement justifies.

What replaced Part 2

  • Rewrote the stale API section of src-springboot/README.md
  • Added ADR 0003

Deliberate deviations

  1. Coverage-driven test selection instead of /tests on a named method β€” see above.
  2. No Javadoc sweep. The classes written across Labs 1–3 already carry Javadoc on public
    members. A blanket /doc pass would have generated restatements of method signatures, which
    the repo instructions explicitly discourage.

Suggested triage

Before the next workshop run β€” Defects 1–3. Neither Part 1 nor Part 2 can be started.

Soon after β€” Defects 4–8. Defect 8 is the priority within this group despite its severity
rating: the PR template teaches participants to assert things they have not verified, and it
denies breaking changes that exist.

When convenient β€” Defects 9–10.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions