Initial commit
This commit is contained in:
+19
@@ -0,0 +1,19 @@
|
||||
## NuGet
|
||||
*.nupkg
|
||||
**/packages/
|
||||
|
||||
## Build
|
||||
bin/
|
||||
obj/
|
||||
|
||||
## IDE
|
||||
*.user
|
||||
*.suo
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
.idea/
|
||||
*.slnx
|
||||
*.slnx launches
|
||||
|
||||
## NuGet local config (workaround for locked ~/.nuget)
|
||||
nuget.config
|
||||
@@ -0,0 +1,17 @@
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderDomain", "src/OrderDomain/OrderDomain.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTests", "tests/OrderTests/OrderTests.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
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.
|
||||
|
||||
## 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
|
||||
* 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 — Refactor Tests with a Custom Matcher
|
||||
|
||||
### 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
|
||||
|
||||
## 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?
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace OrderDomain;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public List<OrderLine> Lines { get; } = new();
|
||||
|
||||
public decimal Total => Lines.Sum(l => l.Value);
|
||||
|
||||
public void AddLine(decimal unitPrice, int quantity)
|
||||
=> Lines.Add(new OrderLine(unitPrice, quantity));
|
||||
|
||||
public void ApplyDiscount(int lineIndex)
|
||||
=> Lines[lineIndex].ApplyDiscount();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace OrderDomain;
|
||||
|
||||
public class OrderLine
|
||||
{
|
||||
public decimal UnitPrice { get; }
|
||||
public int Quantity { get; }
|
||||
public bool HasDiscount { get; private set; }
|
||||
|
||||
public decimal Value =>
|
||||
HasDiscount ? UnitPrice * Quantity * 0.8m : UnitPrice * Quantity;
|
||||
|
||||
public OrderLine(decimal unitPrice, int quantity)
|
||||
{
|
||||
UnitPrice = unitPrice;
|
||||
Quantity = quantity;
|
||||
}
|
||||
|
||||
public void ApplyDiscount() => HasDiscount = true;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework.Constraints;
|
||||
using OrderDomain;
|
||||
|
||||
namespace OrderTests;
|
||||
|
||||
/// <summary>
|
||||
/// Custom NUnit matcher that bundles multiple order assertions into one.
|
||||
/// Participants: you do not need to understand how this works —
|
||||
/// just recognize the pattern and use it as an example for the agent.
|
||||
/// </summary>
|
||||
public static class Has
|
||||
{
|
||||
public static IResolveConstraint OrderState(decimal expectedTotal, decimal[] expectedLineValues)
|
||||
{
|
||||
return new OrderStateConstraint(expectedTotal, expectedLineValues);
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderStateConstraint : Constraint
|
||||
{
|
||||
private readonly decimal _expectedTotal;
|
||||
private readonly decimal[] _expectedLineValues;
|
||||
|
||||
public OrderStateConstraint(decimal expectedTotal, decimal[] expectedLineValues)
|
||||
: base($"order with total {expectedTotal} and line values [{string.Join(", ", expectedLineValues)}]")
|
||||
{
|
||||
_expectedTotal = expectedTotal;
|
||||
_expectedLineValues = expectedLineValues;
|
||||
}
|
||||
|
||||
public override ConstraintResult ApplyTo<TActual>(TActual actual)
|
||||
{
|
||||
var order = (Order)(object)actual!;
|
||||
var actualTotal = order.Total;
|
||||
var actualLineValues = order.Lines.Select(l => l.Value).ToArray();
|
||||
|
||||
var totalMatch = actualTotal == _expectedTotal;
|
||||
var linesMatch = actualLineValues.SequenceEqual(_expectedLineValues);
|
||||
|
||||
var messages = new List<string>();
|
||||
if (!totalMatch)
|
||||
messages.Add($"Total: expected {_expectedTotal}, got {actualTotal}");
|
||||
if (!linesMatch)
|
||||
messages.Add($"Line values: expected [{string.Join(", ", _expectedLineValues)}], got [{string.Join(", ", actualLineValues)}]");
|
||||
|
||||
return new ConstraintResult(this, actual, totalMatch && linesMatch);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\OrderDomain\OrderDomain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,92 @@
|
||||
using NUnit.Framework;
|
||||
using OrderDomain;
|
||||
|
||||
namespace OrderTests;
|
||||
|
||||
public class OrderTotalTests
|
||||
{
|
||||
// This test was refactored — notice the custom matcher.
|
||||
// The other tests still need the same treatment.
|
||||
[Test]
|
||||
public void DiscountOnFirstLine_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200 → 160 with discount
|
||||
order.AddLine(50m, 1); // line 1: 50
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
|
||||
Assert.That(order, Has.OrderState(
|
||||
expectedTotal: 210m,
|
||||
expectedLineValues: new[] { 160m, 50m }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiscountOnSecondLine_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200
|
||||
order.AddLine(50m, 1); // line 1: 50 → 40 with discount
|
||||
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DiscountOnBothLines_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200 → 160
|
||||
order.AddLine(50m, 1); // line 1: 50 → 40
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(200m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(160m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(40m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoDiscount_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(100m, 2); // line 0: 200
|
||||
order.AddLine(50m, 1); // line 1: 50
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(250m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(200m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(50m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SingleLineWithDiscount_TotalAndValue()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(75m, 4); // line 0: 300 → 240
|
||||
|
||||
order.ApplyDiscount(0);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(240m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(240m));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThreeLinesOneDiscount_TotalAndLineValues()
|
||||
{
|
||||
var order = new Order();
|
||||
order.AddLine(20m, 1); // line 0: 20
|
||||
order.AddLine(30m, 2); // line 1: 60 → 48
|
||||
order.AddLine(10m, 3); // line 2: 30
|
||||
|
||||
order.ApplyDiscount(1);
|
||||
|
||||
Assert.That(order.Total, Is.EqualTo(98m));
|
||||
Assert.That(order.Lines[0].Value, Is.EqualTo(20m));
|
||||
Assert.That(order.Lines[1].Value, Is.EqualTo(48m));
|
||||
Assert.That(order.Lines[2].Value, Is.EqualTo(30m));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user