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
Perche' scrivo i test per ultimo, e perche' non e' quello che pensi significhi
**Prendiamo la controversia:**Test-Driven Development è una pratica brillante che risolve il problema sbagliato per la maggior parte del lavoro creativo.
Le tre fasi del software di costruzione (che funziona effettivamente)
E' cosi' che costruisco il software.
E 'semplice, è efficace, e rende i puristi TDD gentilmente solcare le loro sopracciglia:
Fallo funzionare.*Rendilo carino.*Bloccalo con gli esami.
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 quest'ordine.*Sempre.*Non perche' sono pigro.
Non perche' non valgo i test.
questo è il modo in cui il lavoro creativo realmente accade
quando stai esplorando uno spazio problematico non riesci ancora a capire appieno.
quando stai esplorando uno spazio problematico non riesci ancora a capire appieno.
Lascia che ti spieghi.
Questa e' la parte disordinata.La parte in cui non ho idea di cosa sto facendo.
Sto cercando di capire:
Questa API funziona davvero nel modo in cui i documenti sostengono?
Posso risolvere questo problema con gli strumenti che ho?
Che cosa significa "corretto" in questo contesto?*E' un problema di due ore o di due settimane?*Questa e' un'invenzione.
Questa è esplorazione.
Questo e' 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
E sai cosa uccide il jazz?**Qualcuno in piedi sopra la tua spalla che dice "scrivi prima il test."**Perche' ancora nessun test?
Perche'*la forma è ancora fluida.*Immagina di essere uno scultore.
Hai un blocco di marmo e un'idea vaga: "Voglio scolpire un uccello."
Definisci la sua apertura alare.Specificare l'angolo di ciascuna piuma.
Ora scolpisci queste specifiche."*Non e' cosi' che gli uccelli vengono scolpiti (o il software viene scritto, a quanto pare.)*Scultori veri
Per prima cosa distruggi il modulo.Disegnano.
Fanno le maquette.*Tagliano via grossi pezzi di pietra per trovare la forma generale.*Solo più tardi affinano i dettagli.
Il codice e' lo stesso.
Questo codice è
privata.
E' una conversazione con me stesso.
E' cosi' che capisco cosa significhi "lavorare."
attivamente dannoso.
Perché ogni volta che mi rendo conto che "oh aspetta, questo approccio è sbagliato," dovrei riscrivere anche i test.
Quello non e' rigore.
La libertà di fallire velocemente
# 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')
]
La fase 1 è circa
// 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);
}
velocità di apprendimento.
Devo scoprire che la libreria di terze parti che ho pensato di usare... non e' esattamente come pubblicizzato.*Devo capire che il problema che sto risolvendo non e' il problema che sto risolvendo.dovrebbeRisolvere.*I test rallentano.
Non perché il test è lento, ma perché
Allora sono emotivamente coinvolto nella cosa sbagliata.
Meglio fallire velocemente, in privato, senza test da mantenere e senza ego da proteggere.Cosa significa "lavorare" nella fase 1Significa: "L'ho gestita io.
Gli ho dato degli 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
)
Ha prodotto output che sembrano ragionevoli.
// 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();
Sono sicuro al 60% di capire il problema ora."
Tutto qui.**Niente bordelli.**Nessun errore di gestione.
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
*Solo un picco.*Una prova di concetto.
Uno sketch.Fase 2: Rendilo gradevole (Il affinamento)
Ok, ora capisco il problema.Ho qualcosa che funziona, almeno per il percorso felice.
Ora posso iniziare a preoccuparmi della qualita'.
Esempio di Python:*Esempio di C#:*Nome migliore.
Digitare annotazioni.*Documentazione XML.*Implementazione LINQ più pulita.
Allinea con lo specifico
Ora che so cosa sto costruendo, torno ai requisiti originali e mi assicuro di risolvere il problema.
destra
aProblema.
Spesso ciò rivela lacune:
"Oh, volevano che questo gestisse i numeri negativi in modo diverso."
esperienza.**Python:**C#:
Meglio chiamare.
Perche' ancora nessun test?
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, lo gestisco sempre.
Ho una REPL aperta.
Perche'
# 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]
Sto ancora scoprendo cosa vale la pena testare.
Nella Fase 1, non sapevo nemmeno se questa funzione esistesse.Nella Fase 2, sto scoprendo:"Oh, il fattore di scala non può essere negativo.
"Se la lista è vuota, dovremmo tornare vuoti, non Nessuno.""Dobbiamo gestire con grazia i valori non numerici."
Queste intuizioni
emergono dal refactoring.
Non sono ovvi in anticipo.
Il codice e' pulito.*L'API ha senso.*Ho coperto le valigie.
Ora scrivo dei test.
Un sacco di test.
Test di copertura quasi completa come possibili.Perche' adesso?
Perche'
I test sono un meccanismo di condivisione.Non sono piu' per me.
Capisco già questo codice intimamente.
Il gasdotto CI
(che deve prevenire le regressioni)
Manutentori futuri
Le prove sono le seguenti:livello di presentazione
per la mia attuazione.Essi documentano:
Cosa fa questo codiceQuali ingressi sono validiChe risultati ci aspettiamo?
Che supposizioni sto facendo?
Copertura completa, senza compromessi
Modifica con fiducia questa funzione
Prove come documentazione
documentazione eseguibile.
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
**Ma non scrivo test prima che il codice sia pronto a condividere.**Le fasi sono:
Esplorazione privata(solo io, nessun test)
Rifinizione privata
(solo io, ancora nessun test) |-----|-------------| Condividere il pubblico (test, CI, revisione del codice, tutti i nove metri) Questo protegge sia la mia creativitàe | La sanita' mentale dei miei compagni di squadra. Perché questo protegge la creatività
Vero!Ma solo se sai gia' cosa stai costruendo.
Quando sono in fase 1, non ho bisogno di fiducia per rifattore.
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
Mi serve il permesso di buttare via tutto.
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
I test creano debiti psicologici.
Anche se stanno testando la cosa sbagliata.Ritardando le prove fino alla fase 3, mantengo la fase 1 e 2
psicologicamente a buon mercato.
Posso:
Scopri il problema che sono*In realta'*soluzione
È così che funziona la creatività.
// 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 }));
}
Prima fai uno schizzo sciolto.
Non inizi con i dettagli.
// 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);
}
}
Come questo si adatta con Agile e XP (e dove si diverte)
questo non è "test dello sviluppo successivo."
Questo e'
aggiungere test unitari è fondamentale.
Le pratiche XP che tengo
Ogni funzione passa attraverso IC prima della fusione
La raffinatezza collaborativa migliora il designDesign semplice
La fase 2 è
Rifattorizzazione
Fase 2 è dedicato tempo di refactoringPulisci il codiceprima
**I test proteggono il refactoring (nella fase 3+)**Dove diverge da rigido TDD
TDD dice:"Prima le prove.
Sempre.Il fattore verde-rosso è l'unico modo."
Io dico:"Prova quando sai cosa stai testando.
Explore-Refine-Lock è più naturale."
| TDD | Trifase || Test definisce l'interfaccia | Esplorazione definisce l'interfaccia |
| Prova prima di tutto | Prova al momento giusto per ogni cosa |
Tempismo corretto (fase 3):
Stesso risultato qualitativo.*Meta' del churn.*Questo e' Agile.
"Rispondi al cambiamento dopo aver seguito un piano."TDD può diventare la sua forma di piano-seguendo.
Una volta che hai fatto dei test, sei psicologicamente impegnata in quel progetto.
Questo e'di più
Approcci di contrasto: un esempio di C#
Hai notato il hurn?
Tre riscritture di test come il design si è evoluto.
Ricorda i valori:
La fase 1 arriva al software di lavoro
veloce
Collaborare quando aiuta (fase 2-3), esplorare da solo quando non lo fa (fase 1)
La fase 2 si allinea con le specifiche
dopo
Perché:
Non conoscevi ancora i casi piu' importanti.Hai scoperto un design API miglioreHai capito che la funzione non era necessaria per niente
Ogni test che scrivi in fase 1 è probabilmente uno sforzo sprecato.
uno
serie di test in fase 3, quando si sa cosa si sta costruendo, piuttosto che scrivere-riscrivere-riscrivere durante l'esplorazione.Il TDD Promise vs. RealityTDD promette:
"Write code that solves the specification.
Don't worry about perfection yet.
Just get it working."
"I test guidano il tuo design!
Ora sto riscrivendo sia i test che il codice."**Quello non e' design.**E'
Distruggere.Il mio approccio:
Progettare attraverso l'implementazione, quindi bloccarlo con i test.
"Nessun test"*"Spedite e pregate"*Quando ho finito con la Fase 3, il mio codice ha:
Copertura completa dei testCasi di bordo trattati
Pulisci il design delle APIDocumentazione (tramite prove)
Esattamente come TDD.
La differenza è
La regola è semplice:
- flake8 (style checking)
- pylint (code quality)
- mypy (type checking)
- black (formatting)
- radon (complexity analysis)
**Nessun codice lascia la fase 2 senza la fase 3.**Io non
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
Codice di commit non testatoApri PR senza testUnisce alla main senza passare CI
Non e' una nuova idea.È così che il lavoro creativo ha sempre funzionato:
I pittori non iniziano con le pennellate finali.*Essi:*Sketchla composizione (fase 1)
Disegna*i livelli (fase 2)*Smalto
# 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)
"""
)
proteggere e presentare (fase 3)
Essi:Bozzadisordinato (fase 1)
**per la durata (fase 3)**Lo smalto è cruciale.
Come questo mappe per il DiSE Codegen PipelineQui è dove questo diventa interessante: In realtà hoimplementata
questa filosofia nel sistema DiSE (Directed Synthetic Evolution).
Generatore
Nessuna ottimizzazione prematura
Riprovare con maggiore creatività (temperatura più alta)
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
Scalare verso un modello più potente
esattamenteFase 1.
In questa fase non sono state scritte prove.Il codice è privato del processo di generazione.
Fase 2 in DiSE: lo stadio di ottimizzazioneUna volta che il codice passa l'esecuzione di base, DiSE passa aottimizzazione.
(3 iterazioni per impostazione predefinita)
Questo e'esattamente
Il codice è ancora privato (non ancora nel registro), ma lo stiamo lucidando:Nomi migliori
Digita suggerimentiStruttura più pulita
Abbassare la complessità
Migliorare le prestazioniLa forma non è più fluida.
Sappiamo cosa stiamo costruendo.
test formali dell'unità.
Le prove riguardano:*Esempi di specifiche (correttezza)*Casi di bordo scoperti durante la fase 2
Caratteristiche di prestazione misurateAlloraSolo se le prove passano
, il codice è:
Memoria RAG
# 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]
Aggiunto alRegistro dei nodi:
nei flussi di lavoro futuri
punteggi di qualità
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.