- Add OrderBookDomain: in-memory order book with Pending → WaitingForPayment → Fulfilling → Delivered state machine, payment window based on passed-in timestamps, cancellation rules - Add OrderBookTests: 7 integration tests in given/when/then/when/then anti-pattern style as starting material for the exercise - Learning hour: add round 2 where participants create the example themselves before showing it to the agent (no refactored test given) - Fix sln: OrderTests Debug config was mapped to Release
197 lines
8.7 KiB
Markdown
197 lines
8.7 KiB
Markdown
---
|
|
theme: agentic_engineering
|
|
title: Show, Don't Tell — Using Examples with Coding Agents
|
|
name: show_dont_tell
|
|
difficulty: 2
|
|
author: willem
|
|
affiliation:
|
|
tags: agentic copilot refactoring tests examples
|
|
---
|
|
|
|
# Show, Don't Tell — Using Examples with Coding Agents
|
|
|
|
You've described what you want to a coding agent, and it didn't get it right. Or it took so many rounds of corrections that you'd have just done it yourself. This learning hour explores a simpler approach: instead of telling the agent what style you want, show it.
|
|
|
|
## Learning Goals
|
|
|
|
* Use a concrete code example (show, don't tell) to guide a coding agent in propagating a refactoring pattern across a test suite.
|
|
* Produce your own example before delegating: refactor a multi-step test into focused tests yourself, then show the result to the agent as the pattern.
|
|
|
|
## Session Outline
|
|
|
|
* 5 min connect: Frustrating agent conversations
|
|
* 5 min concept: LLMs are pattern matchers
|
|
* 20 min concrete practice: Round 1 — refactor tests using a custom matcher example
|
|
* 15 min concrete practice: Round 2 — split multi-step tests using an example you create
|
|
* 5 min conclusion: Share back and recognize the pattern in your own code
|
|
|
|
## Connect — Frustrating Agent Conversations
|
|
|
|
Pair up and share a time when you had to iterate multiple times with a coding agent just to get it to match the style you wanted. What did you try? How many rounds did it take?
|
|
|
|
This is a [Pair Share]({% link _activities/connect/pair_share.md %}) connect.
|
|
|
|
## Concept — LLMs Are Pattern Matchers
|
|
|
|
LLMs are pattern matchers and translation machines. They are better at recognizing a pattern in concrete code than interpreting abstract instructions.
|
|
|
|
### The "Tell" Approach (doesn't work well)
|
|
|
|
> Refactor these tests to encapsulate the total price check. Instead of asserting on `order.Total`, create a method `IsFullyPaid()` and test for that.
|
|
|
|
The agent might:
|
|
- Create the method but not use it consistently
|
|
- Misinterpret "encapsulate" and change production code instead of test code
|
|
- Apply the pattern incorrectly or only partially
|
|
|
|
### The "Show" Approach (works better)
|
|
|
|
Refactor one test yourself — encapsulate the attribute check behind a behavior method:
|
|
|
|
```csharp
|
|
// Before — checking an attribute
|
|
[Test]
|
|
public void CompletedOrder_TotalIsCorrect()
|
|
{
|
|
var order = new Order();
|
|
order.AddLine(100m, 1);
|
|
order.Complete();
|
|
|
|
Assert.That(order.Total, Is.EqualTo(100m));
|
|
}
|
|
|
|
// After — encapsulating behavior
|
|
[Test]
|
|
public void CompletedOrder_TotalIsCorrect()
|
|
{
|
|
var order = new Order();
|
|
order.AddLine(100m, 1);
|
|
order.Complete();
|
|
|
|
Assert.That(order.IsFullyPaid(100m), Is.True);
|
|
}
|
|
```
|
|
|
|
Then ask the agent:
|
|
|
|
> The first test was refactored to use `order.IsFullyPaid(expected)` instead of asserting on `order.Total`. Apply the same pattern to the remaining tests.
|
|
|
|
The agent sees the concrete pattern and replicates it. No description needed.
|
|
|
|
### Key Insight
|
|
|
|
**Examples beat descriptions.** One concrete example is worth a paragraph of instructions. This works for any refactoring pattern — splitting tests, introducing custom matchers, extracting methods, renaming for clarity. Show the agent what "done" looks like.
|
|
|
|
## Concrete Practice — Round 1 — Refactor Tests with a Custom Matcher (20 min)
|
|
|
|
### Setup
|
|
|
|
Open the project in `exercises/show-dont-tell/` in Rider with GitHub Copilot Agent mode.
|
|
|
|
The project contains an `Order` domain and a test file `OrderTotalTests.cs` with 6 tests.
|
|
|
|
### Step 1 — Identify the Smell (5 min)
|
|
|
|
Open `OrderTotalTests.cs`. Look at the tests. What's the pattern you see?
|
|
|
|
- The first test (`DiscountOnFirstLine_TotalAndLineValues`) was already refactored — notice how it uses `Has.OrderState()` with a single assertion.
|
|
- The remaining 5 tests each have multiple `Assert.That()` calls checking `order.Total`, `order.Lines[0].Value`, `order.Lines[1].Value`, etc.
|
|
|
|
These multiple asserts are checking attributes (state) rather than expressing intention. They're also hard to read and fragile — if the order structure changes, every test needs updating.
|
|
|
|
### Step 2 — Examine the Example (5 min)
|
|
|
|
Look at the first test and the custom matcher file `OrderStateConstraint.cs`. You don't need to understand how the matcher is implemented — just recognize the pattern:
|
|
|
|
```csharp
|
|
// One assertion that bundles all checks
|
|
Assert.That(order, Has.OrderState(
|
|
expectedTotal: 210m,
|
|
expectedLineValues: new[] { 160m, 50m }));
|
|
```
|
|
|
|
vs.
|
|
|
|
```csharp
|
|
// Three separate assertions
|
|
Assert.That(order.Total, Is.EqualTo(240m));
|
|
Assert.That(order.Lines[0].Value, Is.EqualTo(200m));
|
|
Assert.That(order.Lines[1].Value, Is.EqualTo(40m));
|
|
```
|
|
|
|
### Step 3 — Use the Agent (20-25 min)
|
|
|
|
Use GitHub Copilot Agent mode to refactor the remaining 5 tests. Prompt:
|
|
|
|
> The first test uses a custom matcher `Has.OrderState()`. Refactor the remaining tests to use the same pattern — one assertion with `Has.OrderState()` instead of multiple individual asserts.
|
|
|
|
Run the tests after the agent makes changes to verify nothing broke.
|
|
|
|
**If the agent doesn't get it right on the first try:**
|
|
- Check that the example test is visible in the same file
|
|
- Be more specific: "Look at `DiscountOnFirstLine_TotalAndLineValues` as the example"
|
|
- Point out what went wrong and ask it to retry
|
|
|
|
### Tools Needed
|
|
|
|
- Rider with GitHub Copilot Agent mode
|
|
- The `exercises/show-dont-tell/` project (included in this repo)
|
|
- .NET 10 SDK
|
|
|
|
## Concrete Practice — Round 2 — Split Multi-Step Tests (15 min)
|
|
|
|
This round has a twist: **no refactored example is provided.** Creating the example is the exercise.
|
|
|
|
### Setup
|
|
|
|
In the same solution, open the `OrderBookTests` project. The domain is an `OrderBook` with a small state machine:
|
|
|
|
* `OpenOrder(customer)` creates a `Pending` order
|
|
* `FinalizeOrder(id, at)` → `WaitingForPayment` (the timestamp is passed in — no global clock)
|
|
* `PayOrder(id, paidAt)` → `Fulfilling`, unless payment arrives more than 14 days after finalization: the order is `Cancelled` and the payment is rejected
|
|
* `ShipOrder(id)` → `Delivered`
|
|
* `CancelOrder(id)` works from `Pending` and `WaitingForPayment` only; invalid transitions throw
|
|
|
|
`OrderBookLifecycleTests.cs` contains 7 integration tests in an anti-pattern style: **given/when/then/when/then**. Each test walks through several transitions with assertions in between, e.g. `PayThenShip_DeliversTheOrder` finalizes, asserts, pays, ships, and asserts again. The tests pass — they're just testing three behaviors each under a name that describes one.
|
|
|
|
### Step 1 — Identify the Smell (3 min)
|
|
|
|
Look at the tests. What makes them hard to work with?
|
|
|
|
* Multiple act/assert pairs: when one assertion fails, you don't know which transition misbehaved
|
|
* Test names describe one behavior while the test body verifies three
|
|
* Setup for a later transition is entangled with assertions for an earlier one
|
|
|
|
### Step 2 — Create the Example Yourself (7 min)
|
|
|
|
Pick **one** multi-step test and refactor it by hand into focused tests: one transition per test, each test setting up only the state it needs. Keep the exception paths (`Assert.Throws`) in their own tests. Run the tests — the split versions must cover the same behavior as the original.
|
|
|
|
You cannot show the agent what "done" looks like until you have built it. Notice what decisions you had to make: how to name the split tests, what setup helper to extract, where a transition deserves its own test.
|
|
|
|
### Step 3 — Use Your Example with the Agent (5 min)
|
|
|
|
Now show your refactored test to the agent and ask it to apply the same pattern to the remaining tests. Verify with the test run afterward.
|
|
|
|
**If the agent doesn't get it right:**
|
|
|
|
* Check whether your example is unambiguous — could it be read another way?
|
|
* Compare with round 1: what did the provided example communicate that yours doesn't?
|
|
|
|
### Why No Example Was Provided
|
|
|
|
Round 1 showed that examples guide agents better than descriptions. Round 2 practices the harder half of that skill: producing the example. In real work, the pattern you want to propagate usually doesn't exist yet — someone has to write it first, and that someone is you.
|
|
|
|
## Conclusions — Share Back
|
|
|
|
Go around and ask:
|
|
|
|
1. **Did the agent get it right on the first try? What happened if it didn't?**
|
|
2. **What made the example work — what would have made it fail?**
|
|
3. **Do you recognize this pattern in your own tests?** Where could you use "show, don't tell" in your real work?
|
|
4. **What was different in round 2, when you had to create the example yourself?** What made that harder — and what did you learn about writing a good example?
|
|
|
|
This is an [Explain the Main Idea]({% link _activities/conclusions/explain_main_idea.md %}) conclusion.
|
|
|
|
### One-Sentence Takeaway
|
|
|
|
When working with coding agents, show them what you want with a concrete example instead of describing it in words — LLMs are pattern matchers, and examples are patterns. |