Pourquoi j'écris des tests en dernier, et pourquoi ce n'est pas ce que tu penses que ça veut dire
**Prise controversée:**Test-Driven Development est une pratique brillante qui résout le mauvais problème pour le travail le plus créatif.
Les trois phases du logiciel de construction (qui fonctionne réellement)
C'est comme ça que je construis des logiciels.
C'est simple, c'est efficace, et ça rend les puristes TDD poliment sillonner leurs sourcils:
Fais en sorte que ça marche.*Fais-le joliment.*Verrouillez-le avec des tests.
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
Dans cet ordre.*Toujours.*Pas parce que je suis paresseux.
Pas parce que je n'apprécie pas les tests.
C'est comme ça que se passe le travail créatif.
lorsque vous explorez un espace de problèmes que vous ne comprenez pas encore complètement.
lorsque vous explorez un espace de problèmes que vous ne comprenez pas encore complètement.
Laisse-moi t'expliquer.
C'est la partie désordonnée.La partie où je n'ai aucune idée de ce que je fais.
J'essaie de comprendre :
Cette API fonctionne-t-elle vraiment comme l'affirment les docs ?
Puis-je même résoudre ce problème avec les outils que j'ai ?
Qu'est-ce que «correcte» signifie même dans ce contexte?*C'est un problème de deux heures ou de deux semaines ?*C'est de l'invention.
C'est de l'exploration.
C'est du 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
Et tu sais ce qui tue le jazz ?**Quelqu'un se tenant au-dessus de votre épaule disant "écrivez le test d'abord."**Pourquoi pas encore de tests?
Parce que*la forme est encore fluide.*Imaginez que vous êtes sculpteur.
Vous avez un bloc de marbre et une vague idée : "Je veux tailler un oiseau."
Définissez son envergure.Précisez l'angle de chaque plume.
Configurez maintenant ces spécifications."*Ce n'est pas comme ça que les oiseaux sont sculptés. (Ou le logiciel est écrit, comme il se trouve.)*Véritables sculpteurs
D'abord, la forme est rudimentaire.Ils dessinent.
Ils fabriquent des maquettes.*Ils éloignent de gros morceaux de pierre pour trouver la forme générale.*Ce n'est que plus tard qu'ils peaufinent les détails.
Le code est le même.
Ce code est
privé.
C'est une conversation avec moi-même.
C'est comme ça que je comprends ce que "travailler" veut dire.
activement nuisible.
Parce que chaque fois que je réalise "oh attends, toute cette approche est fausse", je devrais réécrire les tests aussi.
Ce n'est pas la rigueur.
La liberté d'échouer rapidement
# 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 phase 1 est d'environ
// 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);
}
vitesse d'apprentissage.
J'ai besoin de découvrir que la bibliothèque tierce que j'ai pensé utiliser... n'est pas tout à fait comme annoncé.*J'ai besoin de comprendre que le problème que je résolve n'est pas le problème que j'ai.devraitRésolvez-vous.*Les tests ralentissent ça.
Pas parce que les tests sont lents, mais parce que
Alors je suis émotionnellement investi dans la mauvaise chose.
Mieux vaut échouer rapidement, en privé, sans aucun test à maintenir et sans ego à protéger.Ce que signifie "travailler" dans la phase 1Ça veut dire : "Je l'ai fait.
Je lui ai donné quelques entrées.
# 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
)
Il a produit des produits qui semblent raisonnables.
// 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();
Je suis sûr de 60 % que je comprends le problème maintenant. »
C'est ça.**Pas de cas de bord.**Pas d'erreur à gérer.
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
*Juste une pointe.*Une preuve de concept.
Un croquis.Phase 2 : Rendre ça joli (Raffinement)
Ok, donc maintenant je comprends le problème.J'ai quelque chose qui fonctionne, du moins pour le chemin heureux.
Maintenant je peux commencer à m'occuper de la qualité.
Exemple de python :*C# Exemple :*Mieux vaut s'appeler.
Tapez des annotations.*Documentation XML.*Mise en œuvre de LINQ plus propre.
Alignez-vous avec la spécification
Maintenant que je sais ce que je construis en fait, je retourne aux exigences d'origine et je m'assure que je résolve le problème.
droite
aproblème.
Cela révèle souvent des lacunes :
"Oh, ils voulaient que cela gère les nombres négatifs différemment."
l'expérience.**Python :**C#:
Mieux vaut nommer.
Pourquoi toujours aucun 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, je le fais constamment.
J'ai un REPL ouvert.
Parce que
# 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]
Je découvre encore ce qui vaut la peine d'être testé.
Dans la phase 1, je ne savais même pas si cette fonction existait.Dans la phase 2, je découvre :"Oh, le facteur d'échelle ne peut pas être négatif.
"Si la liste est vide, nous devrions retourner vide, pas None.""Nous devons gérer avec grâce les valeurs non numériques."
Ces points de vue
En effet, il s'agit là d'un phénomène qui découle d'une refacturation.
Ils ne sont pas évidents à l'avance.
Le code est propre.*L'API est logique.*J'ai couvert les affaires de bord.
Maintenant, j'écris des tests.
Beaucoup de tests.
Les tests sont proches de la couverture complète.Pourquoi maintenant ?
Parce que
Les tests sont un mécanisme de partage.Ils ne sont plus pour moi.
Je comprends déjà ce code intimement.
Le pipeline de l'IC
(qui doit prévenir les régressions)
Mainteneurs futurs
Les essais sont les suivants:couche de présentation
pour ma mise en œuvre.Ils documentent:
Ce que fait ce codeQuelles sont les entrées validesQuels sont les résultats attendus?
Quelles suppositions je fais
Couverture complète, pas de compromis
Modifier cette fonction avec confiance
Essais comme documentation
la documentation exécutable.
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
**Mais je n'écris pas non plus de tests avant que le code ne soit prêt à être partagé.**Les phases sont les suivantes:
Exploration privée(seulement moi, pas de test)
Perfectionnement privé
(seulement moi, toujours pas de tests) |-----|-------------| Partage public (tests, CI, révision de code, tous les neuf verges) Cela protège à la fois ma créativitéet | La santé mentale de mes coéquipiers. Pourquoi cela protège la créativité
C'est vrai !Mais seulement si vous savez déjà ce que vous construisez.
Quand je suis dans la phase 1, je n'ai pas besoin de confiance pour refactorer.
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
J'ai besoin d'une permission pour tout jeter.
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
Les tests créent une dette psychologique.
Même s'ils testent la mauvaise chose.En reportant les essais jusqu'à la phase 3, je maintiens les phases 1 et 2
psychologiquement bon marché.
Je peux :
Découvrez le problème que je suisEn faitrésolution
C'est ainsi que fonctionne la créativité.
// 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 }));
}
Vous esquissez librement d'abord.
Tu ne commences pas avec les détails.
// 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);
}
}
Comment cela s'adapte avec Agile et XP (et où il diverge)
Ce n'est pas "test de développement ultérieur".
C'est ça.
ajouter des tests unitaires est essentiel.
Les pratiques XP que je garde
Chaque fonctionnalité passe par CI avant la fusion
Le raffinement collaboratif améliore la conceptionConception simple
La phase 2 est
Refactoration
La phase 2 est consacrée au temps de refacturationNettoyer le codeavant
**Les essais protègent le refactoring (en phase 3+)**Où je diverge d'un TDD strict
La DNT dit :"Les tests d'abord.
Toujours.Red-Green-Refactor est le seul moyen."
Je dis :"Tests quand vous savez ce que vous testez.
Explore-Refine-Lock est plus naturel."
TDD (Troisième phase)Le test définit l'interface. L'exploration définit l'interface.
Tester d'abord pour tout. Tester au bon moment pour chaque chose.
À l'heure indiquée (phase 3) :
Même résultat de qualité.*La moitié de la courbure.*C'est Agile.
"Répondre au changement en suivant un plan."La DTS peut devenir sa propre forme de suivi des plans.
Une fois que vous avez écrit des tests, vous êtes psychologiquement engagé dans ce design.
C'est ça.plus
Approches contrastées : un exemple C#
Tu as remarqué le churn ?
Trois tests sont réécrits au fur et à mesure de l'évolution de la conception.
Souvenez-vous des valeurs :
La phase 1 arrive au logiciel de travail
rapide
Collaborer quand il aide (phase 2-3), explorer seul quand il ne le fait pas (phase 1)
La phase 2 s'aligne sur les spécifications
après
Parce que :
Vous ne connaissiez pas encore les affaires de bord.Vous avez découvert un meilleur design d'APIVous avez réalisé que la fonctionnalité n'était pas nécessaire du tout
Chaque test que vous écrivez dans la phase 1 est probablement un effort gaspillé.
une
ensemble de tests dans la phase 3, quand vous savez réellement ce que vous construisez, que d'écrire-réécrire-réécrire pendant l'exploration.La promesse de la DTS par rapport à la réalitéTDD promet :
"Write code that solves the specification.
Don't worry about perfection yet.
Just get it working."
"Les tests conduisent votre conception!
Maintenant, je réécris à la fois les tests et le code."**Ce n'est pas du design.**C'est ça.
C'est fou.Mon approche :
Concevoir par l'implémentation, puis le verrouiller avec des tests.
"Pas d'essais"*"Passer et prier"*Au moment où j'en ai fini avec la phase 3, mon code a:
Couverture complète des essaisCas de bord traités
Effacer la conception de l'APIDocumentation (par essais)
Exactement la même chose que TDD.
La différence est de
La règle est simple :
- flake8 (style checking)
- pylint (code quality)
- mypy (type checking)
- black (formatting)
- radon (complexity analysis)
**Aucun code ne quitte la phase 2 sans la phase 3.**Je ne sais pas.
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
Commettre le code non testéOuvrir les PR sans testFusionner vers le secteur sans passer par CI
Ce n'est pas une nouvelle idée.C'est comme ça que le travail créatif a toujours fonctionné :
Les peintres ne commencent pas avec les coups de pinceau finals.*Ils :*Croquisla composition (phase 1)
Peinture*les couches (phase 2)*Vernis
# 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)
"""
)
à protéger et à présent (phase 3)
Ils :Projetdésordre (phase 1)
**pour la durabilité (phase 3)**La glaçure est cruciale.
Comment cela s'inscrit dans le pipeline DiSE CodegenC'est là que ça devient intéressant : en fait, j'aimise en œuvre
cette philosophie dans le système DiSE (Directed Synthetic Evolution).
Groupe électrogène
Pas d'optimisation prématurée
Essayez encore avec plus de créativité (température plus élevée)
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
Escalatez-vous vers un modèle plus puissant
*Exactement.*Phase 1.
Aucun test n'est écrit pendant cette phase.Le code est privé du processus de génération.
Phase 2 de DiSE : l'étape d'optimisationUne fois que le code passe l'exécution de base, DiSE passe àl'optimisation.
(3 itérations par défaut)
C'est ça.Exactement.
Le code est toujours privé (pas encore dans le registre), mais nous le polissons :Meilleurs noms
Type d'indicesStructure plus propre
Faible complexité
Meilleure performanceLa forme n'est plus fluide.
Nous savons ce que nous construisons.
des tests d'unité formels.
Les essais portent sur:*Exemples de spécifications (correctibilité)*Cas de bord découverts au cours de la phase 2
Caractéristiques de performance mesurées*Alors,*seulement si les essais sont réussis
, le code est:
Mémoire 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]
Ajouté à laRegistre des nœuds:
dans les futurs flux de travail
scores de qualité
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.