A hands-on workshop for developers to learn GitHub Copilot using a real-world full-stack application. Covers prompt engineering, code generation, refactoring, test generation, custom instructions, and MCP Playwright for E2E testing.
- Workshop Overview
- Prerequisites
- Project Setup
- Architecture Overview
- Lab 1 - Warm-up: Code Generation with Copilot Chat
- Lab 2 - TDD Kata: String Calculator
- Lab 3 - TDD Kata: Invoice Service
- Lab 4 - Prompt Engineering
- Lab 5 - Refactoring with Copilot
- Lab 6 - Copilot Custom Instructions
- Lab 7 - Generate Tests with Copilot
- Lab 8 - MCP Playwright: E2E Testing
- Tips & Tricks
- Reference: Project Structure
| # | Lab | Topic | Duration |
|---|---|---|---|
| 1 | Warm-up | Copilot Chat basics, inline completions, #workspace |
15 min |
| 2 | String Calculator Kata | TDD with Copilot (Java + TypeScript) | 30 min |
| 3 | Invoice Service Kata | TDD with Copilot (Java + TypeScript) | 20 min |
| 4 | Prompt Engineering | Crafting effective prompts, iterating on output | 25 min |
| 5 | Refactoring | Using Copilot to refactor real production code | 30 min |
| 6 | Custom Instructions | .github/copilot-instructions.md, .instructions.md files |
20 min |
| 7 | Generate Tests | Unit, integration, and API test generation | 25 min |
| 8 | MCP Playwright | E2E browser testing with the Playwright MCP server | 25 min |
Total estimated duration: ~3 hours (adjust based on group size and depth)
| Tool | Version | Purpose |
|---|---|---|
| VS Code | Latest | IDE |
| GitHub Copilot extension | Latest | AI pair programmer |
| GitHub Copilot Chat extension | Latest | Chat interface |
| Java JDK | 21+ | Backend |
| Maven | 3.9+ (or use included ./mvnw) |
Backend build |
| Node.js | 16+ | Frontend |
| npm | 8+ | Frontend dependencies |
| Git | Latest | Version control |
- Ensure you have an active GitHub Copilot license (Individual, Business, or Enterprise)
- Sign in to GitHub in VS Code
- Verify Copilot is active: look for the Copilot icon in the status bar
- Open Copilot Chat with
Ctrl+Shift+I(Windows/Linux) orCmd+Shift+I(macOS)
git clone <repository-url>
cd petclinic./run-all.shBackend (Spring Boot):
cd petclinic-backend
./mvnw spring-boot:runFrontend (Angular):
cd petclinic-frontend
npm install
npm start| Service | URL |
|---|---|
| Frontend | http://localhost:4200 |
| Backend API | http://localhost:8080 |
| Swagger UI | http://localhost:8080/swagger-ui.html |
| H2 Console | http://localhost:8080/h2-console (JDBC URL: jdbc:h2:mem:petclinic, user: sa, no password) |
# Backend
cd petclinic-backend && ./mvnw test
# Frontend
cd petclinic-frontend && npm test┌──────────────────────┐ ┌──────────────────────┐
│ petclinic-frontend │ HTTP │ petclinic-backend │
│ Angular 16 SPA │ ──────► │ Spring Boot 3.5 │
│ Port 4200 │ │ Port 8080 │
└──────────────────────┘ └──────────┬───────────┘
│ JPA
┌──────────▼───────────┐
│ H2 / PostgreSQL │
└──────────────────────┘
Backend layers: REST Controllers → MapStruct Mappers → JPA Repositories → Database
Key packages:
| Package | Contents |
|---|---|
rest/ |
REST controllers (OwnerRestController, VetRestController, ...) |
model/ |
JPA entities (Owner, Pet, Vet, Visit, PetType, Specialty) |
repository/ |
Spring Data JPA repositories |
mapper/ |
MapStruct DTO ↔ Entity mappers |
rest/dto/ |
Generated DTOs from OpenAPI spec |
invoice/ |
Invoice calculation service (exercise) |
util/ |
String Calculator (exercise) |
Frontend modules: Owners, Pets, Vets, Visits, PetTypes, Specialties — each with list/add/edit components and a service.
Goal: Get comfortable with Copilot's different interaction modes.
- Open
petclinic-backend/src/main/java/org/springframework/samples/petclinic/rest/OwnerRestController.java - Place your cursor at the end of the file (inside the class, before the closing
}) - Start typing a comment:
// endpoint to get all pets for a given owner - Observe Copilot's inline suggestion — press
Tabto accept - Try writing another comment and see what Copilot generates
Tip: Press
Alt+]/Option+]to cycle through alternative suggestions.
Open Copilot Chat and try these prompts:
What is the architecture of this project?
How are DTOs mapped to entities in the backend?
Where is the Owner entity defined and what fields does it have?
- Open
petclinic-frontend/src/app/owners/owner-list/owner-list.component.ts - Select the
searchByLastNamemethod - In Copilot Chat, type:
/explain - Try:
Explain the subscription pattern used in this Angular component
- Open any file with linting or compile issues
- Hover over the issue and look for the Copilot "Fix" action
- Or select the problematic code and ask:
/fix
Goal: Practice Test-Driven Development with Copilot. Write tests first, then let Copilot help implement the code.
This exercise exists in both the backend (Java) and the frontend (TypeScript) — pick your preferred language or do both.
Implement a calculate / add function that takes a string of comma-separated numbers and returns their sum.
Rules (from kata-log.rocks/string-calculator-kata):
- An empty string returns
0 - A single number returns that number (
"1"→1) - Two comma-separated numbers return their sum (
"1,2"→3) - Handle any amount of numbers
- Handle newline
\nas a separator ("1\n2,3"→6) - Support custom delimiters:
"//;\n1;2"→3 - Negative numbers throw an exception listing all negatives
Files:
- Implementation:
petclinic-backend/src/main/java/.../util/Calculator.java - Test:
petclinic-backend/src/test/java/.../util/CalculatorTest.java
Steps:
- Open
CalculatorTest.java— it's empty - Start writing a test. Type the following and let Copilot complete it:
@Test void emptyStringReturnsZero() {
- Accept the suggestion, then add more tests one by one (following the kata rules above)
- After each test, switch to
Calculator.javaand let Copilot help implement the logic - Run tests:
cd petclinic-backend ./mvnw test -pl . -Dtest=CalculatorTest
Prompt hints — if Copilot needs guidance, try in chat:
Implement the calculate method in Calculator.java following the String Calculator Kata rules.
The tests in CalculatorTest.java define the expected behavior.
Files:
- Implementation:
petclinic-frontend/src/app/calculator/calculator.ts - Test:
petclinic-frontend/src/app/calculator/calculator.spec.ts
Steps:
- Open
calculator.spec.ts— it has the import but no tests - Write tests incrementally:
describe('String Calculator', () => { it('should return 0 for empty string', () => {
- Let Copilot complete each test, then implement
add()incalculator.ts - Run tests:
cd petclinic-frontend npm test -- --include='**/calculator*'
- How did Copilot handle the TDD cycle (red → green → refactor)?
- Did you need to adjust suggestions? When?
- Compare the Java and TypeScript implementations — did Copilot produce similar approaches?
Goal: A more business-oriented TDD exercise. Calculate invoice totals with discounts and tax.
Implement calculateTotal that:
- Multiplies each item's
price × quantity - Applies the item's
discountpercentage (e.g., 0.10 = 10% off) - Sums all discounted line totals
- Returns the grand total (optionally with VAT via
TaxService)
Files:
petclinic-backend/src/main/java/.../invoice/InvoiceService.java— hascalculateTotalreturningnullpetclinic-backend/src/main/java/.../invoice/Item.java— record withname,price,quantity,discountpetclinic-backend/src/main/java/.../invoice/TaxService.java— provides VAT calculationpetclinic-backend/src/test/java/.../invoice/InvoiceServiceTest.java— empty test class
Steps:
- Open
InvoiceServiceTest.java - Write tests one at a time:
@Test void emptyListReturnsZero() { assertThat(service.calculateTotal(List.of())).isEqualByComparingTo(BigDecimal.ZERO); }
- Let Copilot suggest the next test cases (single item, multiple items, discount, etc.)
- Implement
calculateTotalassisted by Copilot - Run:
./mvnw test -Dtest=InvoiceServiceTest
Files:
petclinic-frontend/src/app/invoice/invoice.service.ts— returns0petclinic-frontend/src/app/invoice/item.model.ts—Iteminterfacepetclinic-frontend/src/app/invoice/tax.service.ts— VAT at 19%petclinic-frontend/src/app/invoice/invoice.service.spec.ts— empty test suite
Steps:
- Open
invoice.service.spec.ts - Write your first test inside the
describeblock - Iterate: write test → implement → refactor
- Run:
npm test -- --include='**/invoice*'
Ask Copilot:
Add a method calculateTotalWithVat that uses TaxService to apply VAT after calculating the subtotal.
Write the test first.
Goal: Learn how to write effective prompts that produce better Copilot output.
Try these prompts in Copilot Chat and compare the results:
Vague ❌:
Add search to the vets page
Specific ✅:
In VetListComponent (petclinic-frontend/src/app/vets/vet-list/vet-list.component.ts),
add a search input that filters the vets table by last name.
Use the existing pattern from OwnerListComponent which already has a searchByLastName method.
Follow the same Angular reactive pattern with debounce.
Observe how the specific prompt produces more accurate, contextual code.
Try assigning Copilot a role:
You are a senior Spring Boot developer. Review the OwnerRestController
and suggest improvements for error handling, validation, and REST best practices.
You are a security auditor. Review the security configuration in petclinic-backend.
What vulnerabilities do you see? How would you fix them?
Break complex tasks into steps:
I want to add pagination to the GET /api/owners endpoint. Let's do this step by step:
1. First, update OwnerRepository to support Spring Data Pageable
2. Then update OwnerRestController.listOwners to accept page/size query parameters
3. Update the OwnerDto response to include pagination metadata
4. Finally update the Angular OwnerService and OwnerListComponent
Start with step 1.
Learn to reference context in your prompts:
# Reference files explicitly
Look at #file:openapi.yaml and generate a new endpoint
for searching visits by date range. Follow the patterns in VisitRestController.
# Reference selections
# (Select a method first, then ask)
Refactor this method to use Java Streams instead of the for-loop.
# Reference terminal output
# (After a test failure)
@terminal Fix the failing test based on the error output.
- What makes a prompt "good"? (Specificity, context, constraints, examples)
- When is it better to use inline completions vs. chat?
- How do you iterate when the first result isn't right?
Goal: Use Copilot to refactor real code in the project. Learn the /fix, inline chat, and chat-based refactoring workflows.
Open petclinic-frontend/src/app/owners/owner-list/owner-list.component.ts.
This component has several code smells:
- Inconsistent formatting (brace style, spacing)
- Deprecated RxJS subscribe pattern (no error handler in
.subscribe()) - Console.log statements left in production code
- Duplicated logic in
searchByLastName
Task: Select the entire searchByLastName method and ask Copilot:
Refactor this method. Remove console.logs, eliminate the if/else duplication,
and use modern RxJS patterns with proper error handling.
Open petclinic-backend/src/main/java/.../rest/OwnerRestController.java.
The updateOwner method manually maps fields one by one:
currentOwner.setAddress(ownerFieldsDto.getAddress());
currentOwner.setCity(ownerFieldsDto.getCity());
currentOwner.setFirstName(ownerFieldsDto.getFirstName());
currentOwner.setLastName(ownerFieldsDto.getLastName());
currentOwner.setTelephone(ownerFieldsDto.getTelephone());Task: Ask Copilot:
Refactor the updateOwner method to use the OwnerMapper (MapStruct) for mapping
the OwnerFieldsDto fields onto the existing Owner entity, instead of manually
setting each field. Check how other controllers handle updates.
Pick any component (e.g., PetListComponent or VisitListComponent) and ask:
Refactor this component to use modern Angular practices:
- Replace subscribe() with the async pipe in the template
- Use typed reactive forms if applicable
- Remove any deprecated patterns
- Place your cursor on a variable or method name
- Press
F2(Rename Symbol) — Copilot can suggest better names - Or ask in chat:
Suggest better variable names for the fields in this component
Use Copilot Edits (the multi-file editing mode) to make a cross-cutting change:
Add proper HTTP status codes to all REST controller methods in petclinic-backend.
Currently some methods return void — they should return ResponseEntity with
appropriate status codes (200, 201, 204, 404).
Goal: Configure Copilot to follow your team's conventions automatically.
Create .github/copilot-instructions.md at the project root:
mkdir -p .githubThen create the file with content like:
## Project Conventions
### Backend (Java / Spring Boot)
- Use Java 21 features (records, pattern matching, sealed classes) where appropriate
- Follow Spring Boot 3.x conventions
- Use constructor injection via Lombok `@RequiredArgsConstructor` (never field injection)
- Use MapStruct for all DTO ↔ Entity mappings
- Use `ResponseEntity` with proper HTTP status codes in controllers
- Use Bean Validation annotations (`@NotEmpty`, `@Valid`) for input validation
- Write tests with JUnit 5 + AssertJ assertions (not Hamcrest)
- Use `BigDecimal` for monetary values, never `double` or `float`
### Frontend (Angular / TypeScript)
- Use Angular 16 conventions
- Use RxJS operators and the `async` pipe — avoid manual `.subscribe()` where possible
- Use Angular Material components for UI elements
- Use TypeScript strict mode — no `any` types
- Service methods should return `Observable<T>`, not `Promise<T>`
### General
- Prefer immutability — use `final` in Java, `const`/`readonly` in TypeScript
- No console.log in production code
- Write descriptive test names that explain the expected behaviorTest it: After saving, open a controller and ask Copilot to add a new endpoint. Verify it follows the conventions (constructor injection, ResponseEntity, etc.)
Create a .instructions.md file scoped to the test folder:
Create petclinic-backend/src/test/.instructions.md:
---
applyTo: "**/*Test.java"
---
## Test Conventions
- Use JUnit 5 (`@Test` from `org.junit.jupiter.api`)
- Use AssertJ for assertions (`assertThat(...).isEqualTo(...)`)
- Use `@ExtendWith(MockitoExtension.class)` for mocked tests
- Use `@WebMvcTest` for controller tests with MockMvc
- Test method names: `should_ExpectedBehavior_When_Condition()`
- Use `@DisplayName` for readable test descriptions
- Arrange-Act-Assert pattern in every testCreate petclinic-frontend/src/.instructions.md:
---
applyTo: "**/*.spec.ts"
---
## Angular Test Conventions
- Use Jasmine `describe`/`it` blocks
- Use Angular TestBed for component tests
- Mock HTTP calls with HttpClientTestingModule
- Test names should read as sentences: `it('should display owners when loaded')`
- Use `fakeAsync`/`tick` for async operationsTest it: Open a test file and ask Copilot to generate a new test. Verify it follows the conventions.
- Try adding a rule that intentionally conflicts (e.g., "use Hamcrest assertions") and see how Copilot resolves it
- Add a rule about code comments (e.g., "never add comments that explain what — only why")
- Ask Copilot to generate code with and without the instructions file — compare the results
- How do custom instructions differ from putting conventions in a prompt each time?
- What's the right level of specificity? Too strict vs. too loose?
- How would you roll these out across a team?
Goal: Use Copilot to generate comprehensive tests for existing code.
- Open
petclinic-backend/src/main/java/.../rest/OwnerRestController.java - In Copilot Chat:
/tests Generate comprehensive unit tests for OwnerRestController. Use @WebMvcTest, MockMvc, and mock the repositories with Mockito. Cover: list all owners, get by ID, create owner with validation, update owner, delete owner, and error cases (not found). - Review the generated tests — do they compile? Are assertions meaningful?
- Run the tests and fix any issues with Copilot's help
- Open
petclinic-frontend/src/app/owners/owner-list/owner-list.component.ts - Ask:
/tests Generate Jasmine tests for OwnerListComponent. Mock OwnerService using a spy. Test: initial load fetches owners, search by last name calls the service, empty search reloads all owners, and error handling.
Use the OpenAPI specification to generate tests:
Based on #file:openapi.yaml, generate integration tests for the
/api/owners endpoints. Use @SpringBootTest with TestRestTemplate.
Cover all status codes defined in the spec (200, 201, 400, 404).
Select an existing test file and ask:
Review these existing tests and suggest missing edge cases.
What scenarios are not covered? Generate tests for them.
The project includes jqwik (a property-based testing library). Try:
This project uses jqwik for property-based testing.
Write property-based tests for the Calculator.calculate method
that verify: commutativity of addition, identity element (adding 0),
and that the result is always >= 0 for positive inputs.
Goal: Use the Playwright MCP server to write end-to-end browser tests by having Copilot interact with the running application.
-
Make sure the application is running (backend on :8080, frontend on :4200)
-
Install Playwright MCP Server — add to your VS Code
settings.json:{ "mcp": { "servers": { "playwright": { "command": "npx", "args": ["@playwright/mcp@latest"] } } } } -
Restart VS Code to activate the MCP server
In Copilot Chat (Agent mode), ask:
Using the Playwright MCP tools, navigate to http://localhost:4200
and take a screenshot. Describe what you see on the page.
Then:
Navigate to the Owners page, take a screenshot, and describe
the table structure and available actions.
Using Playwright MCP, perform the following on http://localhost:4200:
1. Navigate to the Owners page
2. Click "Add Owner"
3. Fill in the form with: First Name = "John", Last Name = "Copilot",
Address = "123 AI Street", City = "Redmond", Telephone = "1234567890"
4. Submit the form
5. Verify the new owner appears in the owners list
Take a screenshot after each step.
After exploring the app, ask Copilot to generate a reusable test:
Based on the screenshots and page structure you've seen, generate a
Playwright test script (TypeScript) that:
1. Opens the PetClinic app
2. Navigates to Owners
3. Searches for owners with last name "Davis"
4. Clicks on the first result
5. Verifies the owner details page shows the correct name
6. Adds a new pet to this owner
Save it as petclinic-frontend/e2e/owners.spec.ts
Using Playwright MCP:
1. Navigate to http://localhost:4200/owners
2. Take a screenshot
3. Search for "Franklin"
4. Take another screenshot
5. Compare what changed on the page and describe the differences
Using Playwright MCP, test the complete CRUD lifecycle:
1. Create a new owner
2. Add a pet to the owner
3. Add a visit for the pet
4. Verify all data is displayed correctly
5. Delete the owner
6. Verify the owner no longer appears in the list
Report any bugs or UI issues you discover.
- How does MCP Playwright compare to writing E2E tests manually?
- When would you use this approach vs. traditional Playwright scripts?
- What are the limitations of AI-driven E2E testing?
| Action | Windows/Linux | macOS |
|---|---|---|
| Accept suggestion | Tab |
Tab |
| Dismiss suggestion | Esc |
Esc |
| Next suggestion | Alt+] |
Option+] |
| Previous suggestion | Alt+[ |
Option+[ |
| Open Copilot Chat | Ctrl+Shift+I |
Cmd+Shift+I |
| Inline Chat | Ctrl+I |
Cmd+I |
| Command | Purpose |
|---|---|
/explain |
Explain selected code |
/fix |
Fix issues in selected code |
/tests |
Generate tests for selected code |
/doc |
Generate documentation |
#workspace |
Ask about the entire codebase |
@terminal |
Reference terminal output |
#file:path |
Reference a specific file |
#selection |
Reference selected code |
| Technique | Example |
|---|---|
| Be specific | "Add a findByCity method to OwnerRepository returning List<Owner>" |
| Provide context | "Following the pattern in VetRestController, add a delete endpoint to..." |
| Set constraints | "Use only Java 21 features. No external libraries." |
| Give examples | "Like this: assertThat(result).isEqualTo(expected)" |
| Assign a role | "You are a Spring Security expert. Review this configuration." |
| Iterate | "Good, but also handle the case where the list is empty" |
| Layer | Technology | Version |
|---|---|---|
| Backend | Spring Boot | 3.5.9 |
| Backend | Java | 21 |
| Backend | Spring Data JPA | via Spring Boot |
| Backend | MapStruct | 1.6.3 |
| Backend | Lombok | 1.18.36 |
| Backend | OpenAPI / Swagger | springdoc 2.8.15 |
| Frontend | Angular | 16.2.1 |
| Frontend | Angular Material | 16.2.1 |
| Frontend | TypeScript | 4.9.5 |
| Frontend | RxJS | 6.3.1 |
| Database | H2 (dev) / PostgreSQL | Embedded / 16.3 |
| QA | Selenium | 4.31.0 |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/owners |
List owners (optional ?lastName= filter) |
| GET | /api/owners/{id} |
Get owner by ID |
| POST | /api/owners |
Create owner |
| PUT | /api/owners/{id} |
Update owner |
| DELETE | /api/owners/{id} |
Delete owner |
| GET | /api/pets |
List all pets |
| GET | /api/pets/{id} |
Get pet by ID |
| PUT | /api/pets/{id} |
Update pet |
| DELETE | /api/pets/{id} |
Delete pet |
| GET | /api/vets |
List all vets |
| POST | /api/vets |
Create vet |
| PUT | /api/vets/{id} |
Update vet |
| DELETE | /api/vets/{id} |
Delete vet |
| GET | /api/pettypes |
List pet types |
| POST | /api/pettypes |
Create pet type |
| GET | /api/specialties |
List specialties |
| POST | /api/specialties |
Create specialty |
| GET | /api/visits |
List visits |
| POST | /api/visits |
Create visit |
| POST | /api/users |
Create user |
H2 (Default) — In-memory, auto-populated at startup
- Console: http://localhost:8080/h2-console
- JDBC URL:
jdbc:h2:mem:petclinic| User:sa| Password: (blank)
PostgreSQL — spring.profiles.active=postgres
cd petclinic-backend && docker-compose --profile postgres upDisabled by default. Enable with petclinic.security.enable=true.
Roles: OWNER_ADMIN, VET_ADMIN, ADMIN | Default credentials: admin / admin