💡 Also available: .NET version · Kotlin version · Swift version
Duration: 15 minutes
Learning Objectives:
- Generate comprehensive test suites using
/testscommand - Create documentation with
/doccommand - Write Conventional Commit messages with AI assistance
- Draft PR descriptions using
@workspacefor full context - Integrate Copilot into complete development workflow
This lab brings together everything you've learned by focusing on the "glue" activities that complete the development lifecycle:
- Testing - Generate comprehensive test coverage
- Documentation - Create clear, maintainable docs
- Version Control - Write meaningful commit messages
- Code Review - Prepare thorough PR descriptions
These activities are often rushed or skipped, but Copilot makes them fast and consistent.
⚠️ Golden rule for this lab: Copilot will happily invent class names, test counts, coverage percentages and "no breaking changes" claims. Every fact in a test, doc, commit message or PR description must be checked against the real code, the real test run, and the realgit diff.
Keep these in mind when reviewing anything Copilot generates:
| Item | Real value |
|---|---|
Task statuses (TaskStatus) |
PENDING, IN_PROGRESS, COMPLETED, CANCELLED |
| Build tool | Maven (mvn) — there is no mvnw and no Gradle build |
| Domain package | com.example.taskmanager.domain.tasks (Task, TaskId, TaskStatus, TaskRepository) |
| Application package | com.example.taskmanager.application.services (TaskService) |
| API DTO package | com.example.taskmanager.api.dto (CreateTaskRequest, UpdateTaskRequest, TaskResponse records) |
- ✅ Completed Labs 1, 2, and 3 (Java versions)
- ✅ Working Task Manager API with CRUD operations
- ✅ Git initialized with commits from previous labs
- ✅ Familiar with all Copilot features (chat, inline chat, slash commands, context variables)
You have basic tests from TDD, but need comprehensive coverage including edge cases, integration tests, and error scenarios.
Open src-springboot/taskmanager-application/src/main/java/com/example/taskmanager/application/services/TaskService.java and select the createTask(String title, String description) method.
TaskService is a single concrete class holding all task use cases. It already has a test class (TaskServiceTest), so /tests is asked to find what is missing, not to start from zero.
With the method selected, open Copilot Chat (Ctrl+Alt+I / Cmd+Shift+I) and enter:
/tests Add missing edge cases for createTask(String, String): title trimming, title longer than 200 characters, null description. Use JUnit 5 and Mockito, follow the style of the existing TaskServiceTest. Do not duplicate existing tests.
Copilot should generate tests like these (inside the existing CreateTaskOperations nested class of TaskServiceTest, which already declares the @Mock TaskRepository taskRepository and @InjectMocks TaskService taskService):
@Test
@DisplayName("should trim title before saving")
void shouldTrimTitleBeforeSaving() {
// Arrange
when(taskRepository.save(any(Task.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
// Act
Task result = taskService.createTask(" Write docs ", "Lab 4");
// Assert
assertEquals("Write docs", result.getTitle());
assertEquals(TaskStatus.PENDING, result.getStatus());
}
@Test
@DisplayName("should allow a null description")
void shouldAllowNullDescription() {
when(taskRepository.save(any(Task.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
Task result = taskService.createTask("Write docs", null);
assertNull(result.getDescription());
}
@Test
@DisplayName("should reject a title longer than 200 characters")
void shouldRejectTitleLongerThan200Characters() {
String tooLong = "x".repeat(201);
assertThrows(IllegalArgumentException.class,
() -> taskService.createTask(tooLong, null));
verify(taskRepository, never()).save(any(Task.class));
}
@Test
@DisplayName("should not touch the repository when the title is null")
void shouldNotSaveWhenTitleIsNull() {
assertThrows(NullPointerException.class,
() -> taskService.createTask(null, "description"));
verify(taskRepository, never()).save(any(Task.class));
}What to check in anything Copilot generates here:
- ✅ No builders or setters.
Taskhas a private constructor and is created withTask.create(title)/Task.create(title, description). Its status is assigned by the aggregate (PENDINGon creation) and changes only throughstart(),complete()andcancel(). - ✅ Stubbing returns a real value.
thenAnswer(invocation -> invocation.getArgument(0))echoes the saved task. Never writethenReturn(any(Task.class))— a matcher is not a value, and Mockito throwsInvalidUseOfMatchersException(often reported on a different, later stubbing). - ✅ No unused variables left over from generation.
- ✅ Status names exist. Only
PENDING,IN_PROGRESS,COMPLETED,CANCELLED. - ✅ Imports resolve to
com.example.taskmanager.domain.tasks.*andcom.example.taskmanager.application.services.TaskService.
Instead of picking a method by hand, measure first, then write the tests the measurement justifies:
cd src-springboot
mvn clean test jacoco:reportOpen taskmanager-<module>/target/site/jacoco/index.html for each module, find a class with low coverage (for example in taskmanager-infrastructure), select it, and run /tests on it. Compare with src-springboot/coverage-summary.txt, which records an earlier snapshot — numbers go stale, so always re-measure.
Open src-springboot/taskmanager-api/src/test/java/com/example/taskmanager/api/TaskApiIntegrationTest.java (it already uses @SpringBootTest, @AutoConfigureMockMvc and H2) and ask Copilot Chat:
Add integration tests to TaskApiIntegrationTest for the POST /api/tasks validation rules: blank title returns 400, title over 200 characters returns 400, and a valid request returns 201 with status PENDING. Match the existing test style (MockMvc, ObjectMapper).
Expected shape (build requests with the CreateTaskRequest record — records are immutable, so create a new request per case instead of mutating a shared fixture):
@Test
@DisplayName("POST /api/tasks creates a PENDING task")
void shouldCreateTaskWithPendingStatus() throws Exception {
CreateTaskRequest request = CreateTaskRequest.of("Write docs", "Lab 4");
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("Write docs"))
.andExpect(jsonPath("$.status").value("PENDING"));
}
@Test
@DisplayName("POST /api/tasks rejects a blank title")
void shouldRejectBlankTitle() throws Exception {
CreateTaskRequest request = CreateTaskRequest.of(" ");
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
@Test
@DisplayName("POST /api/tasks rejects a title over 200 characters")
void shouldRejectTitleOver200Characters() throws Exception {
CreateTaskRequest request = CreateTaskRequest.of("x".repeat(201));
mockMvc.perform(post("/api/tasks")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}Review checklist: record accessors are title() / status() (not getTitle()); TaskResponse.status is a String such as "PENDING"; there are no builders or setters on the DTOs. The existing test class already contains some of these cases — delete any generated duplicates.
The Spring Boot track is Maven-only and has no wrapper, so use plain mvn from src-springboot/:
cd src-springboot
mvn clean testVerify coverage includes:
- ✅ Happy path tests
- ✅ Guard clause tests
- ✅ Edge case tests (nulls, empty strings, boundaries)
- ✅ Validation tests
- ✅ Error handling tests
- ✅ Integration tests
Create accurate documentation for the Task Manager API.
Open src-springboot/taskmanager-application/src/main/java/com/example/taskmanager/application/services/TaskService.java and select the createTask(String title, String description) method (or the class declaration).
With the selection active, use Inline Chat (Ctrl+I):
/doc
What good output looks like — it explains behavior and failure modes, not just the signature:
/**
* Creates a new task in PENDING status and persists it.
*
* @param title the task title; trimmed, must not be blank or exceed 200 characters
* @param description optional task description, may be null
* @return the persisted task
* @throws NullPointerException if title is null
* @throws IllegalArgumentException if title is blank or too long
*/The Javadoc in TaskService already covers most public methods. If Copilot only restates the signature, add business context to the prompt or skip the method — a comment that repeats the code is noise.
Records have accessors, not getters: when documenting DTOs, refer to request.title() and response.status().
src-springboot/README.md already has an "API Endpoints" section. Update it rather than adding a second one. Ask:
@workspace Compare the API Endpoints section in src-springboot/README.md with TaskController, CreateTaskRequest, UpdateTaskRequest and TaskResponse. List anything that is missing or wrong, then propose corrections. Only document what exists in the code.
For reference, here is the real API that the README must match (verify against TaskController):
### Endpoints
Base URL: `http://localhost:8080/api/tasks`
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/tasks` | Create a task (starts as `PENDING`) |
| `GET` | `/api/tasks` | List all tasks (plain JSON array, no filtering or paging) |
| `GET` | `/api/tasks/{id}` | Get a task by ID |
| `PATCH` | `/api/tasks/{id}` | Partially update title and/or description |
| `PUT` | `/api/tasks/{id}/start` | `PENDING` → `IN_PROGRESS` |
| `PUT` | `/api/tasks/{id}/complete` | → `COMPLETED` |
| `PUT` | `/api/tasks/{id}/cancel` | → `CANCELLED` |
| `DELETE` | `/api/tasks/{id}` | Delete a task |
**Create a task**
```bash
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Write docs", "description": "Finish Lab 4"}'
```
Request fields: `title` (required, max 200 characters), `description` (optional, max 2000 characters).
Response `201 Created`:
```json
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Write docs",
"description": "Finish Lab 4",
"status": "PENDING",
"createdAt": "2026-01-15T10:30:00",
"completedAt": null
}
```
**Status values**: `PENDING`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED`
**Errors** use RFC 7807 Problem Details, for example `404 Not Found`:
```json
{
"type": "https://api.taskmanager.example.com/problems/task-not-found",
"title": "Task Not Found",
"status": 404,
"detail": "Task not found with ID: 123e4567-e89b-12d3-a456-426614174000"
}
```If Copilot documents query filters, a paged envelope, a completed flag, a PUT update, or status values like TODO/DONE, it is describing a different API. Correct it.
ADR 0001 – Use Clean Architecture already exists. Do not recreate or overwrite it. Check docs/adr/ for the next free number (currently 0002) and document a decision you actually made during the labs. A good candidate: TaskService is a concrete class rather than an interface-per-use-case. Ask Copilot:
Create an Architecture Decision Record at docs/adr/0002-<short-decision-title>.md, following the format of docs/adr/0001-use-clean-architecture.md. The decision: TaskService is a single concrete application service (no interface, no CQRS, no mediator library). Base the context on src-springboot/taskmanager-application/.../services/TaskService.java and .github/instructions/springboot.instructions.md. Include Context, Decision, Consequences (positive and negative) and Alternatives considered. Do not invent facts that are not in the code.
Any decision from your own work is fine — the point is a fresh decision with real trade-offs.
You've made changes across several files. Write meaningful commit messages following the Conventional Commits format.
First see what you actually changed, then stage those real paths:
git status
git diff --statFor example, if you completed Parts 1 and 2:
git add src-springboot/taskmanager-application/src/test/java/com/example/taskmanager/application/services/TaskServiceTest.java
git add src-springboot/taskmanager-api/src/test/java/com/example/taskmanager/api/TaskApiIntegrationTest.java
git add src-springboot/README.md docs/adr/git add on a path that does not exist fails with pathspec ... did not match any files — if you see that, you are staging something you did not create.
In Copilot Chat:
Write a Conventional Commit message for the staged changes. Use only what appears in `git diff --staged`. Include a concise subject line (<=72 chars) and a short body explaining what was added and why.
Example of a faithful result (yours will differ — it must describe your diff):
test(application): cover createTask edge cases
- Add tests for title trimming, null description and 200-character limit
- Add POST /api/tasks validation tests to TaskApiIntegrationTest
- Align README API section with TaskController
The existing tests only covered the happy path and null/blank titles.
Verify before committing: every bullet maps to a file in the diff. Do not accept class names, handlers, test counts or metrics (such as "reduces complexity from 15 to 4") that Copilot cannot point to in the diff — inventing them is the failure mode this step is meant to teach you to catch.
git commit -m "test(application): cover createTask edge cases" \
-m "Add tests for title trimming, null description and the 200-character limit. Add POST /api/tasks validation tests. Align the README API section with TaskController."If you refactored LegacyTaskProcessor in Lab 3, stage it and its tests (use git status to find the real test path):
git add src-springboot/taskmanager-infrastructure/src/main/java/com/example/taskmanager/infrastructure/legacy/LegacyTaskProcessor.java
git statusAsk Copilot:
Write a Conventional Commit message for the staged LegacyTaskProcessor refactoring. Describe only the changes visible in `git diff --staged`.
Check the result against the diff: if the message says "Stream API" but the code uses a StringBuilder loop, or quotes complexity figures nobody measured, fix the message. If your change alters behavior, mark it with ! and a BREAKING CHANGE: footer.
You've completed your changes. Create an honest PR description for review.
In Copilot Chat:
@workspace Draft a Pull Request description for the changes on this branch compared with main. Base it on `git diff main...HEAD` only. Include:
- Summary of changes
- What was implemented (features, refactoring, tests)
- Intent and motivation
- Testing performed (list the exact commands and their results)
- Breaking changes (list them, or state how you verified there are none)
- Checklist for reviewers
Do not state test counts, coverage percentages or performance figures unless they come from a command output I provide. Use Markdown formatting.
Example of an honest result (fill in the numbers from your own run):
# Add createTask edge-case tests and align README (Spring Boot)
## Summary
Adds unit and integration tests for task creation validation and corrects the
API section of `src-springboot/README.md` to match `TaskController`.
## Changes
- `TaskServiceTest`: trimming, null description, 200-character title limit
- `TaskApiIntegrationTest`: POST /api/tasks validation cases
- `README.md`: endpoint table and examples now match the code
- `docs/adr/0002-...`: records the "concrete TaskService" decision
## Testing Performed
```bash
cd src-springboot
mvn clean test
```
Result: <paste BUILD SUCCESS/FAILURE and the "Tests run" total from your output>
Coverage: <paste figures from `mvn test jacoco:report`, or write "not measured">
## Breaking Changes
None to production code — this PR only adds tests and documentation.
(If you changed an API contract, list each change here.)
## Known Gaps
- No load or performance testing was done.
## Reviewer Checklist
- [ ] README endpoints match `TaskController`
- [ ] New tests fail when the behavior they cover is broken
- [ ] ADR reflects a decision actually made in the code
⚠️ Verify every claim. An assistant will confidently write "45+ tests", "92% coverage", "no memory leaks detected" or "Breaking Changes: None" with nothing behind it. Before posting, check each factual statement against command output and the diff. "Breaking Changes: None" is only acceptable if you checked that no API contract, database schema or public behavior changed.
Review the generated PR description and adjust:
- Replace placeholders with real command output (test totals, coverage)
- Add specific issue numbers
- Delete anything you cannot verify
- Highlight any specific areas needing review
- /tests Command: Generates test suites quickly — but you must verify they compile against the real API
- Measure, Then Test: JaCoCo shows where real gaps are
- Test Organization: Clear package structure, descriptive test names
- Integration Tests: @SpringBootTest with MockMvc for full API testing
- Mockito: Stub with real values (
thenAnswer), never matchers as return values
- /doc Command: Javadoc generated from code context
- API Docs: Update the existing README section and check it against the controller
- Architecture Docs: ADRs document decisions you really made, at the next free number
- Consistency: Use one status list everywhere:
PENDING,IN_PROGRESS,COMPLETED,CANCELLED
- Conventional Commits: Structured, parsable commit messages
- Semantic Commits: Type, scope, description format
- Faithful Bodies: Describe only what is in the diff
- Changelog Ready: Commits can generate CHANGELOG.md automatically
- @workspace Context: Full codebase understanding for the PR description
- Honest PRs: Real command output, no invented metrics
- Reviewer Checklist: Clear acceptance criteria
- Impact Analysis: Breaking changes stated only after verification
Ask Copilot to generate a CHANGELOG.md file from your commit history:
Generate a CHANGELOG.md file based on the git commit history. Group by version, follow Keep a Changelog format.
Generate CONTRIBUTING.md with guidelines for contributors:
Create a CONTRIBUTING.md file that explains:
- How to set up the development environment
- Coding conventions (reference .github/instructions/ for context-aware standards)
- Testing requirements
- PR process
- Commit message format
Generate documentation for consuming the API:
Create a quick start guide in docs/guides/api-quickstart.md for developers consuming our Task Manager API. Include endpoint examples with Java RestTemplate or WebClient, and common error handling patterns. Only document endpoints that exist in TaskController.
You've completed this lab successfully when:
- ✅ New tests generated with
/testscompile and pass (unit + integration) - ✅ Public APIs you touched have meaningful Javadoc via
/doc - ✅ README API section matches
TaskController - ✅ Conventional Commit messages describe only real changes
- ✅ PR description drafted with
@workspaceand every claim verified - ✅
mvn clean testpasses - ✅ Documentation is clear and maintainable
- ✅ Ready for code review
Congratulations! You've completed all four labs (Java version). You now know how to:
- Follow Red-Green-Refactor cycle
- Use Copilot to generate tests before implementation
- Apply Copilot Instructions for consistent code quality
- Decompose user stories into backlog items
- Generate acceptance criteria
- Implement features using TDD
- Maintain Clean Architecture across all layers
- Generate complete REST endpoints with context
- Refactor legacy code with
/refactor - Apply Object Calisthenics principles
- Use Copilot Edits for multi-file changes
- Generate comprehensive test coverage
- Create clear documentation
- Write meaningful commit messages
- Prepare thorough, verified PR descriptions
Problem: Tests don't cover all edge cases
Solution: Be explicit: "/tests including edge cases, validation errors, and null handling"
Problem: Generated tests use builders, setters, getTitle() on records, or status values such as TODO/DONE
Solution: Point Copilot at the real types (Task.create(...), CreateTaskRequest.of(...), record accessors like title()) and the real TaskStatus values
Problem: Javadoc comments don't add value beyond method signature
Solution: Select more context (class + method), provide business context in prompt
Problem: Copilot generates "Update files" messages, or describes classes and metrics that are not in the diff
Solution: Stage related changes only and tell Copilot to use git diff --staged as the only source
Problem: PR description is too high-level
Solution: Use @workspace and be specific: "Include testing details, breaking changes, and reviewer checklist"
Problem: @SpringBootTest tests fail with bean creation errors
Solution: Ensure test configuration matches main configuration, check application-test.yml
- Add context-aware instruction files (
.github/instructions/) to your team's repositories - Establish Conventional Commits standard
- Use
/testsfor all new code - Use
/docfor public APIs - Use
@workspacein daily work
- Custom instructions for team-specific patterns
- Copilot for Business with organization policies
- Fine-tuned models for domain-specific code
- Integration with CI/CD pipelines
- Practice TDD with different features
- Explore advanced DDD patterns
- Learn Spring Boot Actuator for observability
- Study Clean Architecture in depth
- Explore Spring Cloud for microservices