From e8748ee69791b46b125a0f84891f351d53ff2af8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 3 Sep 2026 13:50:38 +0000 Subject: [PATCH] Add round 2: split multi-step OrderBook tests (example created by participant) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- exercises/show-dont-tell/ShowDontTell.sln | 16 ++- exercises/show-dont-tell/learning-hour.md | 50 +++++++- .../src/OrderBookDomain/Order.cs | 25 ++++ .../src/OrderBookDomain/OrderBook.cs | 69 +++++++++++ .../OrderBookDomain/OrderBookDomain.csproj | 7 ++ .../OrderBookTests/OrderBookLifecycleTests.cs | 114 ++++++++++++++++++ .../OrderBookTests/OrderBookTests.csproj | 16 +++ 7 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 exercises/show-dont-tell/src/OrderBookDomain/Order.cs create mode 100644 exercises/show-dont-tell/src/OrderBookDomain/OrderBook.cs create mode 100644 exercises/show-dont-tell/src/OrderBookDomain/OrderBookDomain.csproj create mode 100644 exercises/show-dont-tell/tests/OrderBookTests/OrderBookLifecycleTests.cs create mode 100644 exercises/show-dont-tell/tests/OrderBookTests/OrderBookTests.csproj diff --git a/exercises/show-dont-tell/ShowDontTell.sln b/exercises/show-dont-tell/ShowDontTell.sln index a8db231..42d40d3 100644 --- a/exercises/show-dont-tell/ShowDontTell.sln +++ b/exercises/show-dont-tell/ShowDontTell.sln @@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderDomain", "src/OrderDom EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTests", "tests/OrderTests/OrderTests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderBookDomain", "src/OrderBookDomain/OrderBookDomain.csproj", "{C3D4E5F6-A7B8-9012-CDEF-123456789012}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderBookTests", "tests/OrderBookTests/OrderBookTests.csproj", "{D4E5F6A7-B8C9-0123-DEFA-234567890123}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,8 +22,14 @@ Global {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Deploy.0 = Release|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3D4E5F6-A7B8-9012-CDEF-123456789012}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4E5F6A7-B8C9-0123-DEFA-234567890123}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Deploy.0 = Debug|Any CPU EndGlobalSection EndGlobal diff --git a/exercises/show-dont-tell/learning-hour.md b/exercises/show-dont-tell/learning-hour.md index 1b9448b..b49093c 100644 --- a/exercises/show-dont-tell/learning-hour.md +++ b/exercises/show-dont-tell/learning-hour.md @@ -15,12 +15,14 @@ You've described what you want to a coding agent, and it didn't get it right. Or ## 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 -* 35 min concrete practice: Refactor tests using a custom matcher example +* 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 @@ -80,7 +82,7 @@ The agent sees the concrete pattern and replicates it. No description needed. **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 — Refactor Tests with a Custom Matcher +## Concrete Practice — Round 1 — Refactor Tests with a Custom Matcher (20 min) ### Setup @@ -136,6 +138,49 @@ Run the tests after the agent makes changes to verify nothing broke. - 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: @@ -143,6 +188,7 @@ 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. diff --git a/exercises/show-dont-tell/src/OrderBookDomain/Order.cs b/exercises/show-dont-tell/src/OrderBookDomain/Order.cs new file mode 100644 index 0000000..8fb4293 --- /dev/null +++ b/exercises/show-dont-tell/src/OrderBookDomain/Order.cs @@ -0,0 +1,25 @@ +namespace OrderBookDomain; + +public enum OrderStatus +{ + Pending, + WaitingForPayment, + Fulfilling, + Delivered, + Cancelled +} + +public class Order +{ + public int Id { get; } + public string Customer { get; } + public OrderStatus Status { get; internal set; } + public DateTimeOffset? FinalizedAt { get; internal set; } + + internal Order(int id, string customer) + { + Id = id; + Customer = customer; + Status = OrderStatus.Pending; + } +} diff --git a/exercises/show-dont-tell/src/OrderBookDomain/OrderBook.cs b/exercises/show-dont-tell/src/OrderBookDomain/OrderBook.cs new file mode 100644 index 0000000..6ce8098 --- /dev/null +++ b/exercises/show-dont-tell/src/OrderBookDomain/OrderBook.cs @@ -0,0 +1,69 @@ +namespace OrderBookDomain; + +public class OrderBook +{ + private readonly TimeSpan _paymentWindow; + private readonly Dictionary _ordersById = new(); + private int _nextId = 1; + + public OrderBook(TimeSpan? paymentWindow = null) + => _paymentWindow = paymentWindow ?? TimeSpan.FromDays(14); + + public IReadOnlyList Orders => _ordersById.Values.ToList(); + + public Order OpenOrder(string customer) + { + var order = new Order(_nextId++, customer); + _ordersById[order.Id] = order; + return order; + } + + public Order GetOrder(int orderId) + => _ordersById.TryGetValue(orderId, out var order) + ? order + : throw new InvalidOperationException($"Unknown order id {orderId}."); + + public void FinalizeOrder(int orderId, DateTimeOffset finalizedAt) + { + var order = GetOrder(orderId); + RequireStatus(order, OrderStatus.Pending, "finalize"); + order.Status = OrderStatus.WaitingForPayment; + order.FinalizedAt = finalizedAt; + } + + public void PayOrder(int orderId, DateTimeOffset paidAt) + { + var order = GetOrder(orderId); + RequireStatus(order, OrderStatus.WaitingForPayment, "pay"); + if (paidAt - order.FinalizedAt > _paymentWindow) + { + order.Status = OrderStatus.Cancelled; + throw new InvalidOperationException( + $"Payment for order {orderId} arrived after the payment window; the order was cancelled."); + } + order.Status = OrderStatus.Fulfilling; + } + + public void ShipOrder(int orderId) + { + var order = GetOrder(orderId); + RequireStatus(order, OrderStatus.Fulfilling, "ship"); + order.Status = OrderStatus.Delivered; + } + + public void CancelOrder(int orderId) + { + var order = GetOrder(orderId); + if (order.Status is not (OrderStatus.Pending or OrderStatus.WaitingForPayment)) + throw new InvalidOperationException( + $"Cannot cancel order {orderId} in status {order.Status}."); + order.Status = OrderStatus.Cancelled; + } + + private static void RequireStatus(Order order, OrderStatus expected, string action) + { + if (order.Status != expected) + throw new InvalidOperationException( + $"Cannot {action} order {order.Id} in status {order.Status} (expected {expected})."); + } +} diff --git a/exercises/show-dont-tell/src/OrderBookDomain/OrderBookDomain.csproj b/exercises/show-dont-tell/src/OrderBookDomain/OrderBookDomain.csproj new file mode 100644 index 0000000..6c3a887 --- /dev/null +++ b/exercises/show-dont-tell/src/OrderBookDomain/OrderBookDomain.csproj @@ -0,0 +1,7 @@ + + + net10.0 + enable + enable + + diff --git a/exercises/show-dont-tell/tests/OrderBookTests/OrderBookLifecycleTests.cs b/exercises/show-dont-tell/tests/OrderBookTests/OrderBookLifecycleTests.cs new file mode 100644 index 0000000..599af5f --- /dev/null +++ b/exercises/show-dont-tell/tests/OrderBookTests/OrderBookLifecycleTests.cs @@ -0,0 +1,114 @@ +using NUnit.Framework; +using OrderBookDomain; + +namespace OrderBookTests; + +public class OrderBookLifecycleTests +{ + private static readonly DateTimeOffset Day0 = new(2025, 6, 1, 0, 0, 0, TimeSpan.Zero); + + private static DateTimeOffset At(int day) => Day0.AddDays(day); + + [Test] + public void FinalizeThenPay_MovesOrderThroughTwoStates() + { + var book = new OrderBook(); + var order = book.OpenOrder("anna"); + + book.FinalizeOrder(order.Id, At(1)); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment)); + + book.PayOrder(order.Id, At(2)); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Fulfilling)); + } + + [Test] + public void PayThenShip_DeliversTheOrder() + { + var book = new OrderBook(); + var order = book.OpenOrder("anna"); + + book.FinalizeOrder(order.Id, At(1)); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment)); + + book.PayOrder(order.Id, At(2)); + book.ShipOrder(order.Id); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered)); + } + + [Test] + public void PayWithinWindow_ThenLatePayOnSecondOrder_CancelsOnlyThatOrder() + { + var book = new OrderBook(); + var first = book.OpenOrder("anna"); + var second = book.OpenOrder("bram"); + + book.FinalizeOrder(first.Id, At(1)); + book.FinalizeOrder(second.Id, At(1)); + Assert.That(book.Orders, Has.Count.EqualTo(2)); + + book.PayOrder(first.Id, At(5)); + Assert.That(book.GetOrder(first.Id).Status, Is.EqualTo(OrderStatus.Fulfilling)); + + Assert.Throws(() => book.PayOrder(second.Id, At(30))); + Assert.That(book.GetOrder(second.Id).Status, Is.EqualTo(OrderStatus.Cancelled)); + } + + [Test] + public void PayBeforeFinalize_Throws_ThenFinalizeStillWorks() + { + var book = new OrderBook(); + var order = book.OpenOrder("anna"); + + Assert.Throws(() => book.PayOrder(order.Id, At(1))); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Pending)); + + book.FinalizeOrder(order.Id, At(2)); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment)); + } + + [Test] + public void CancelPendingOrder_ThenPayAfterCancel_ThrowsAgain() + { + var book = new OrderBook(); + var order = book.OpenOrder("anna"); + + book.CancelOrder(order.Id); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled)); + + Assert.Throws(() => book.PayOrder(order.Id, At(1))); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled)); + } + + [Test] + public void ShippedOrder_CannotBeCancelled() + { + var book = new OrderBook(); + var order = book.OpenOrder("anna"); + + book.FinalizeOrder(order.Id, At(1)); + book.PayOrder(order.Id, At(2)); + book.ShipOrder(order.Id); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered)); + + Assert.Throws(() => book.CancelOrder(order.Id)); + Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered)); + } + + [Test] + public void TwoCustomers_InterleaveTransitions_Independently() + { + var book = new OrderBook(); + var annas = book.OpenOrder("anna"); + var brams = book.OpenOrder("bram"); + + book.FinalizeOrder(brams.Id, At(1)); + Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Pending)); + Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment)); + + book.PayOrder(brams.Id, At(2)); + book.CancelOrder(annas.Id); + Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Cancelled)); + Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.Fulfilling)); + } +} diff --git a/exercises/show-dont-tell/tests/OrderBookTests/OrderBookTests.csproj b/exercises/show-dont-tell/tests/OrderBookTests/OrderBookTests.csproj new file mode 100644 index 0000000..71067ec --- /dev/null +++ b/exercises/show-dont-tell/tests/OrderBookTests/OrderBookTests.csproj @@ -0,0 +1,16 @@ + + + net10.0 + enable + enable + true + + + + + + + + + +