This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Wednesday, 19 November 2025
Waarom ik de laatste test schrijf, en waarom dat niet is wat jij denkt dat het betekent.
**Controversiële opname:**Test-Driven Development is een briljante praktijk die het verkeerde probleem voor de meeste creatieve werk oplost.
De drie fasen van de bouwsoftware (dat werkt eigenlijk)
Zo bouw ik software.
Het is eenvoudig, het is effectief, en het maakt TDD puristen beleefd groeven hun wenkbrauwen:
Zorg dat het werkt.*Maak het mooi.*Sluit het af met testen.
graph TB
Start[New Feature Request] --> P1[Phase 1: Make It Work]
P1 --> P1_Private[Private Exploration]
P1_Private --> P1_Sketch["Sketch the solution<br/>Messy code OK<br/>No tests yet<br/>Focus on learning"]
P1_Sketch --> P1_Run["Run it manually<br/>Try different inputs<br/>See what breaks<br/>Understand the problem"]
P1_Run --> P1_Decision{Does it work<br/>basically?}
P1_Decision -->|No| P1_Iterate[Try different approach]
P1_Iterate --> P1_Sketch
P1_Decision -->|Yes| P2
P2[Phase 2: Make It Pretty] --> P2_Refine[Private Refinement]
P2_Refine --> P2_Clean["Clean up the code<br/>Better names<br/>Type hints/annotations<br/>Clear structure"]
P2_Clean --> P2_Align["Align with spec<br/>Cover all requirements<br/>Handle edge cases<br/>Polish API surface"]
P2_Align --> P2_Optimise["Optimise<br/>Performance<br/>Readability<br/>Maintainability"]
P2_Optimise --> P3
P3[Phase 3: Lock It Down] --> P3_Share[Public Sharing]
P3_Share --> P3_Tests["Write comprehensive tests<br/>Full coverage<br/>Edge cases<br/>Error conditions"]
P3_Tests --> P3_CI["CI/CD Integration<br/>Automated testing<br/>Code review<br/>Documentation"]
P3_CI --> P3_Deploy["Ready to Share<br/>Commit to main<br/>Deploy to prod<br/>Other devs can use it"]
P3_Deploy --> Done[Well-Tested,<br/>Well-Designed Code]
style P1 stroke:#ff6b6b,stroke-width:3px
style P2 stroke:#4ecdc4,stroke-width:3px
style P3 stroke:#95e1d3,stroke-width:3px
style Done stroke:#38ada9,stroke-width:4px
In die volgorde.*Altijd.*Niet omdat ik lui ben.
Niet omdat ik geen testen waardeer.
Dit is hoe creatief werk daadwerkelijk gebeurt
Als je een probleemruimte verkent die je nog niet volledig begrijpt.
Als je een probleemruimte verkent die je nog niet volledig begrijpt.
Laat het me uitleggen.
Dit is het rommelige gedeelte.Het deel waar ik geen idee heb wat ik doe.
Ik probeer het te begrijpen:
Werkt deze API echt zoals de dokters beweren?
Kan ik dit probleem oplossen met het gereedschap dat ik heb?
Wat betekent "correct" in deze context?*Is dit een probleem van twee uur of twee weken?*Dit is uitvinding.
Dit is verkenning.
Dit is jazz.
# Ugly, exploratory code
def process_thing(data):
# TODO: This is terrible, fix later
result = []
for item in data:
# Not sure if this is the right approach...
x = item.get('value')
if x: # Is this even the right condition?
result.append(x * 2) # Why multiply by 2? Testing something...
return result
En weet je wat jazz doodt?**Iemand die over je schouder staat en zegt: "Schrijf eerst de test."**Waarom nog geen tests?
Omdat*De vorm is nog steeds vloeibaar.*Stel je voor dat je beeldhouwer bent.
Je hebt een blok marmer en een vaag idee: "Ik wil een vogel snijden."
Definieer zijn spanwijdte.Specificeer de hoek van elke veer.
Snijd nu naar die specificaties."*Dat is... niet hoe vogels gekerfd worden. (Of software wordt geschreven, zo blijkt.)*Echte beeldhouwers
Eerst de vorm uitvegen.Ze schetsen.
Ze maken maquettes.*Ze verscheuren grote stukken steen om de algemene vorm te vinden.*Pas later verfijnen ze de details.
Code is hetzelfde.
Deze code is
Privé.
Het is een gesprek met mezelf.
Het is hoe ik erachter kom wat "werken" zelfs betekent.
actief schadelijk.
Want elke keer als ik me realiseer "oh wacht, deze hele aanpak is verkeerd," zou ik de tests ook moeten herschrijven.
Dat is geen rigor.
De vrijheid om snel te falen
# Before (Phase 1) - Exploratory code
def process_thing(data):
result = []
for item in data:
x = item.get('value')
if x:
result.append(x * 2)
return result
# After (Phase 2) - Refined code
def extract_doubled_values(items: list[dict]) -> list[int]:
"""Extract 'value' field from items and double each one.
Only includes items where 'value' is present and truthy.
"""
return [
item['value'] * 2
for item in items
if item.get('value')
]
Fase 1 gaat over
// Before (Phase 1) - Exploratory code
public List<int> ProcessThing(List<Dictionary<string, object>> data)
{
var result = new List<int>();
foreach (var item in data)
{
if (item.ContainsKey("value"))
{
var x = item["value"];
if (x != null)
{
result.Add(Convert.ToInt32(x) * 2);
}
}
}
return result;
}
// After (Phase 2) - Refined code
/// <summary>
/// Extracts 'value' field from items and doubles each one.
/// Only includes items where 'value' is present and non-null.
/// </summary>
public IEnumerable<int> ExtractDoubledValues(IEnumerable<IDictionary<string, object>> items)
{
return items
.Where(item => item.ContainsKey("value") && item["value"] != null)
.Select(item => Convert.ToInt32(item["value"]) * 2);
}
Leersnelheid.
Ik moet ontdekken dat de bibliotheek die ik dacht te gebruiken... niet zo geadverteerd is.*Ik moet me realiseren dat het probleem dat ik oplos niet het probleem is.moetom het op te lossen.*Tests vertragen dit.
Niet omdat testen traag is, maar omdat
Dan ben ik emotioneel betrokken bij de verkeerde dingen.
Beter om snel te falen, privé, zonder tests om te behouden en zonder ego om te beschermen.Wat "werken" betekent in fase 1Het betekent: "Ik heb het gerund.
Ik gaf het wat input.
# Phase 1: Works, but awkward to use
process_thing(data)
# Phase 2: Clear, flexible, composable
extract_doubled_values(
items,
scale_factor=2.0,
include_zeros=False
)
Het produceerde producties die redelijk lijken.
// Phase 1: Works, but awkward to use
ProcessThing(data);
// Phase 2: Clear, flexible, composable
ExtractDoubledValues(
items,
scaleFactor: 2.0,
includeZeros: false
);
// Or even better with a fluent API
items.ExtractValues()
.Scale(by: 2.0)
.ExcludingZeros()
.ToList();
Ik ben 60% ervan overtuigd dat ik het probleem nu begrijp."
Dat is het.**Geen randgevallen.**Geen fout bij het afhandelen.
graph LR
subgraph "Phase 2 Refinement Process"
A[Rough Code] --> B[Clean Structure]
B --> C[Add Types]
C --> D[Polish API]
D --> E[Static Analysis]
E --> F{Quality Gates Pass?}
F -->|No| G[Fix Issues]
G --> B
F -->|Yes| H[Ready for Phase 3]
end
style A stroke:#ff6b6b,stroke-width:3px
style H stroke:#95e1d3,stroke-width:3px
*Gewoon een spike.*Een bewijs van het concept.
Een schets.Fase 2: Maak het mooi (de verfijning)
Oké, dus nu begrijp ik het probleem.Ik heb iets dat werkt, tenminste voor het gelukkige pad.
Nu kan ik beginnen met zorgen te maken over kwaliteit.
Python Voorbeeld:*C# Voorbeeld:*Betere naam.
Typ annotaties.*XML documentatie.*Cleaner LINQ implementatie.
Uitlijnen met de spec
Nu ik weet wat ik eigenlijk aan het bouwen ben, ga ik terug naar de oorspronkelijke vereisten en zorg ervoor dat ik het oplossen van de
rechts
aprobleem.
Dikwijls vertoont dit hiaten:
"Oh, ze wilden dat dit negatieve getallen anders zou behandelen."
ervaring.**Python:**C#:
Betere naamgeving.
Waarom nog steeds geen tests?
def test_extract_doubled_values_basic():
items = [{'value': 5}, {'value': 10}]
assert extract_doubled_values(items) == [10, 20]
def test_extract_doubled_values_skips_missing_values():
items = [{'value': 5}, {'name': 'foo'}, {'value': 3}]
assert extract_doubled_values(items) == [10, 6]
def test_extract_doubled_values_handles_zeros():
items = [{'value': 0}, {'value': 5}]
# By default, zeros are excluded (falsy)
assert extract_doubled_values(items) == [10]
def test_extract_doubled_values_includes_zeros_when_configured():
items = [{'value': 0}, {'value': 5}]
assert extract_doubled_values(items, include_zeros=True) == [0, 10]
def test_extract_doubled_values_custom_scale_factor():
items = [{'value': 5}]
assert extract_doubled_values(items, scale_factor=3.0) == [15.0]
def test_extract_doubled_values_rejects_negative_scale_factor():
with pytest.raises(ValueError):
extract_doubled_values([], scale_factor=-1.0)
def test_extract_doubled_values_handles_empty_list():
assert extract_doubled_values([]) == []
def test_extract_doubled_values_handles_non_numeric_values():
items = [{'value': 'not a number'}]
with pytest.raises(TypeError):
extract_doubled_values(items)
Oh, ik doe het constant.
Ik heb een repl open.
Omdat
# This comment will drift from reality:
# Doubles all numeric values, skipping None
# This test will fail if reality changes:
def test_extract_doubled_values_skips_none_values():
items = [{'value': None}, {'value': 5}]
assert extract_doubled_values(items) == [10]
Ik ben nog steeds aan het ontdekken wat het testen waard is.
In fase 1 wist ik niet of deze functie zou bestaan.In fase 2 ontdek ik:"Oh, de schaalfactor kan niet negatief zijn.
"Als de lijst leeg is, moeten we leeg retourneren, niet geen.""We moeten gracieus omgaan met niet-numerieke waarden."
Deze inzichten
tevoorschijn komen uit refactoring.
Ze zijn niet voor de hand liggend.
De code is schoon.*De API is logisch.*Ik heb de randgevallen onderzocht.
Nu schrijf ik tests.
Veel testen.
Zo dicht mogelijk bij volledige dekking-zo-mogelijke tests.Waarom nu?
Omdat
tests zijn een gedeeld mechanisme.Ze zijn niet meer voor mij.
Ik begrijp deze code al intiem.
De CI Pipeline
(die regressies moet voorkomen)
Toekomstige onderhouders
De proeven zijn depresentatielaag
voor mijn implementatie.Zij documenteren:
Wat deze code doetWelke inputs geldig zijnWelke outputs te verwachten zijn
Welke veronderstellingen maak ik?
Volledige dekking, geen compromissen
Wijzig deze functie vol vertrouwen
Tests als documentatie
Uitvoerbare documentatie.
graph TB
subgraph "Traditional TDD (Red-Green-Refactor)"
TDD1[Write Failing Test] --> TDD2[Write Minimal Code to Pass]
TDD2 --> TDD3[Refactor]
TDD3 --> TDD1
end
subgraph "Three-Phase Approach (Explore-Refine-Lock)"
P1[Explore Solution Space] --> P2[Refine Implementation]
P2 --> P3[Write Comprehensive Tests]
P3 --> P4[Share With Team]
end
style TDD1 stroke:#ffaa00,stroke-width:2px
style TDD2 stroke:#ffaa00,stroke-width:2px
style TDD3 stroke:#ffaa00,stroke-width:2px
style P1 stroke:#ff6b6b,stroke-width:3px
style P2 stroke:#4ecdc4,stroke-width:3px
style P3 stroke:#95e1d3,stroke-width:3px
**Maar ik schrijf ook geen testen voordat de code klaar is om te delen.**De fasen zijn:
Particuliere exploratie(alleen ik, geen testen)
Particuliere verfijning
(alleen ik, nog steeds geen testen) |-----|-------------| Delen van het publiek (tests, CI, code herziening, de hele negen yards) Dit beschermt mijn creativiteit.en | De gezondheid van mijn teamgenoten. Waarom dit Creativiteit beschermt
Klopt!Maar alleen als je al weet wat je aan het bouwen bent.
Als ik in fase 1 zit, heb ik geen vertrouwen nodig om te refactoreren.
Hour 1: Write test for API I haven't designed yet
Hour 2: Realize the API is wrong, rewrite test
Hour 3: Discover a better approach, rewrite both
Hour 4: Finally get it working
Hour 5: Rewrite tests to match what actually works
Ik heb toestemming nodig om alles weg te gooien.
Hour 1: Explore 3 different APIs, settle on the best one
Hour 2: Refine the implementation, handle edge cases
Hour 3: Write comprehensive tests for what I built
Hour 4: All tests pass, commit with confidence
Tests leiden tot psychologische schulden.
Zelfs als ze het verkeerde testen.Door tests uit te stellen tot fase 3, houd ik fase 1 en 2
psychologisch goedkoop.
Ik kan:
Ontdek het probleem dat ik benEigenlijkoplossen
Zo werkt creativiteit.
// Test 1: Write test first (but I don't know the right API yet)
[Test]
public void TestProcessData()
{
var processor = new DataProcessor();
var result = processor.Process(new[] { 1, 2, 3 });
Assert.That(result, Is.EqualTo(new[] { 2, 4, 6 }));
}
// Implementation 1: Make it pass
public class DataProcessor
{
public int[] Process(int[] data) => data.Select(x => x * 2).ToArray();
}
// Later: Realize I need configurability, rewrite test
[Test]
public void TestProcessDataWithFactor()
{
var processor = new DataProcessor();
var result = processor.Process(new[] { 1, 2, 3 }, factor: 3);
Assert.That(result, Is.EqualTo(new[] { 3, 6, 9 }));
}
// Later: Realize I should use IEnumerable, rewrite test again
[Test]
public void TestProcessDataEnumerable()
{
var processor = new DataProcessor();
var data = GetLargeDataset(); // This should be lazy
var result = processor.Process(data, factor: 3);
Assert.That(result.Take(3), Is.EqualTo(new[] { 3, 6, 9 }));
}
Je schetst eerst losjes.
Je begint niet met de details.
// Phase 1: Explore (no tests yet)
public int[] ProcessThing(int[] data)
{
return data.Select(x => x * 2).ToArray();
}
// Try it: var result = ProcessThing(new[] { 1, 2, 3 });
// Hmm, what about configurability? What about lazy evaluation?
// Phase 2: Refine (still no tests)
public static class DataProcessorExtensions
{
public static IEnumerable<int> Scale(
this IEnumerable<int> source,
int factor = 2)
{
return source.Select(x => x * factor);
}
}
// Try it: var result = data.Scale(factor: 3).ToList();
// Much better API!
// Phase 3: Lock it down (NOW write tests, correctly)
[TestFixture]
public class DataProcessorTests
{
[Test]
public void Scale_DefaultFactor_DoublesValues()
{
var result = new[] { 1, 2, 3 }.Scale();
Assert.That(result, Is.EqualTo(new[] { 2, 4, 6 }));
}
[Test]
public void Scale_CustomFactor_ScalesCorrectly()
{
var result = new[] { 1, 2, 3 }.Scale(factor: 3);
Assert.That(result, Is.EqualTo(new[] { 3, 6, 9 }));
}
[Test]
public void Scale_LazyEvaluation_DoesNotEnumerateImmediately()
{
var enumerated = false;
var data = GetTestData(() => enumerated = true);
var scaled = data.Scale(factor: 2);
Assert.That(enumerated, Is.False, "Should not enumerate yet");
scaled.ToList();
Assert.That(enumerated, Is.True, "Should enumerate on materialization");
}
[Test]
public void Scale_EmptySequence_ReturnsEmpty()
{
var result = Enumerable.Empty<int>().Scale();
Assert.That(result, Is.Empty);
}
}
Hoe dit past met agile en XP (en waar het diverges)
Dit is geen "test later ontwikkeling."
Dit is
het toevoegen van unit tests is cruciaal.
De XP praktijken die ik houd
Elke functie gaat via CI alvorens te mergen
Collaboratieve verfijning verbetert ontwerpEenvoudig ontwerp
Fase 2 is
Refactoring
Fase 2 is gewijde refactoring tijdDe code opruimenvoor
**Tests beschermen de refactoring (in fase 3+)**Waar ik duik van Strikte TDD
TDD zegt:'Test eerst.
Altijd.Red-Green-Refactor is de enige manier."
Ik zeg:"Tests als je weet wat je test.
Explore-Refine-Lock is natuurlijker."
Drie fases.De test definieert de interface . . . . . . . . . de interface bepaalt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Test eerst voor alles Test op het juiste moment voor elk ding
Juiste timing (fase 3):
Zelfde kwaliteit resultaat.*De helft van de karn.*Dit IS Agile
"Reageer op verandering over het volgen van een plan."TDD kan zijn eigen vorm van plan-volgend worden.
Zodra je testen hebt geschreven, ben je psychologisch toegewijd aan dat ontwerp.
Dit ismeer
Contrasting Approaches: A C# Example
Zie je de karn?
Drie tests herschrijven naarmate het ontwerp evolueerde.
Onthoud de waarden:
Fase 1 gaat naar werkende software
snel
Samenwerken wanneer het helpt (fase 2-3), alleen verkennen als het niet (fase 1)
Fase 2 komt overeen met de specificaties
na
Omdat:
Je wist nog niet van de randgevallen.U ontdekte een beter API ontwerpJe besefte dat het niet nodig was.
Elke test die je schrijft in fase 1 is waarschijnlijk verspilde moeite.
één
Testen in fase 3, als je echt weet wat je aan het bouwen bent, dan schrijven-herschrijven-herschrijven tijdens de exploratie.De TDD Belofte vs. RealiteitTDD belooft:
"Write code that solves the specification.
Don't worry about perfection yet.
Just get it working."
"Tests rijden uw ontwerp!
Nu herschrijf ik zowel de tests als de code."**Dat is geen ontwerp.**Dat is
thrashen.Mijn aanpak:
Ontwerpen door implementatie, dan afsluiten met testen.
"Geen tests"*"Verbergen en bidden"*Tegen de tijd dat ik klaar ben met fase 3, heeft mijn code:
Uitgebreide testdekkingBehandelde Rand-gevallen
Helder API-ontwerpDocumentatie (via tests)
Precies hetzelfde als TDD.
Het verschil is
De regel is simpel:
- flake8 (style checking)
- pylint (code quality)
- mypy (type checking)
- black (formatting)
- radon (complexity analysis)
**Geen code verlaat fase 2 zonder fase 3.**Ik weet het niet.
for iteration in range(3):
# Measure current performance
metrics = measure_performance(code)
# Generate improved version
better_code = optimise(code, metrics)
# Keep if better, discard if worse
if better_code.score > code.score:
code = better_code
Niet-geteste code committenOpen PR's zonder testsSamenvoegen naar hoofdvak zonder CI-passage
Dit is geen nieuw idee.Het is hoe creatief werk altijd heeft gewerkt:
Schilders beginnen niet met de laatste penseelstreken.*Zij:*Sketchde samenstelling (fase 1)
Schilderen*de lagen (fase 2)*Varnish
# Test generation happens AFTER optimisation
def generate_unit_tests(specification, optimized_code):
"""Generate comprehensive tests for refined code.
This happens in Phase 3, after we know:
- What the code actually does
- What edge cases exist
- What the API surface looks like
"""
return llm.generate(
f"""Generate comprehensive unit tests for this code.
Specification: {specification}
Implementation: {optimized_code}
Include tests for:
- Happy path (from spec examples)
- Edge cases (discovered during optimisation)
- Error conditions (based on actual error handling)
- Performance bounds (based on measured metrics)
"""
)
om te beschermen en aanwezig te zijn (fase 3)
Zij:Ontwerprommelig (fase 1)
**voor duurzaamheid (fase 3)**Het glazuur is cruciaal.
Hoe deze kaarten naar de DiSE Codegen PipelineHier is waar dit interessant wordt: ik heb eigenlijkgeïmplementeerd
deze filosofie in het DiSE (Directed Synthetic Evolution) systeem.
Generator
Geen premature optimalisatie
Probeer het opnieuw met hogere creativiteit (hogere temperatuur)
User: "Calculate fibonacci numbers"
[Phase 1: Exploration - 3 attempts]
Attempt 1: Works for small inputs, explodes on large ones (no safety limit)
Attempt 2: Adds safety limit, but inefficient recursive approach
Attempt 3: Switches to iterative DP (PASS)
[Phase 2: Optimization - 3 iterations]
Iteration 1: Add type hints, improve naming (Score: 1.05)
Iteration 2: Optimize memory usage with generator (Score: 1.10)
Iteration 3: Add input validation (Score: 1.15)
[Phase 3: Testing and Storage]
Generated 8 unit tests covering:
- Basic cases (n=0, n=1, n=5, n=10)
- Edge cases (n=negative, n=100, n=None)
- Type validation
All tests pass ✓
Stored in RAG with quality score: 1.15
Available for reuse: YES
Escaleer naar een krachtiger model
preciesFase 1.
Tijdens deze fase worden geen tests uitgevoerd.De code is privé aan het generatieproces.
Fase 2 in DiSE: OptimalisatiefaseZodra de code de basisuitvoering passeert, verplaatst DiSE naaroptimalisatie.
(3 iteraties standaard)
Dit isprecies
De code is nog privé (nog niet in het register), maar we polijsten het:Betere namen
Type hintsSchonere structuur
Lagere complexiteit
Betere prestatiesDe vorm is niet langer vloeibaar.
We weten wat we bouwen.
formele eenheidstests.
De tests hebben betrekking op:*Specificatievoorbeelden (correctheid)*Randgevallen ontdekt tijdens fase 2
Gemeten prestatiekenmerken*Dan,*alleen als de tests slagen
, de code is:
RAG-geheugen
# I know what sort() should do. Tests first? Sure!
def test_sort_empty_list():
assert sort([]) == []
def test_sort_single_element():
assert sort([5]) == [5]
def test_sort_multiple_elements():
assert sort([3, 1, 2]) == [1, 2, 3]
Toegevoegd aan denode register:
in toekomstige workflows
kwaliteitsscores
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.