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
¿Por qué escribo las últimas pruebas, y por qué eso no es lo que crees que significa
**Toma controversial:**Test-Driven Development es una práctica brillante que resuelve el problema equivocado para la mayoría del trabajo creativo.
Las tres fases del software de construcción (que realmente funciona)
Así es como construyo el software.
Es simple, es efectivo, y hace que los puristas de TDD frunzan educadamente sus cejas:
Haz que funcione.*Que sea bonito.*Enciérrenlo con pruebas.
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
En ese orden.*Siempre.*No porque sea perezoso.
No porque no valoro las pruebas.
así es como el trabajo creativo realmente sucede
Cuando estás explorando un espacio problemático aún no lo entiendes completamente.
Cuando estás explorando un espacio problemático aún no lo entiendes completamente.
Déjame explicarte.
Esta es la parte desordenada.La parte en la que no tengo ni idea de lo que estoy haciendo.
Estoy tratando de entender:
¿Esta API realmente funciona de la manera en que los documentos afirman?
¿Puedo incluso resolver este problema con las herramientas que tengo?
¿Qué significa "corregir" incluso en este contexto?*¿Es un problema de dos horas o un problema de dos semanas?*Esto es invención.
Esto es exploración.
Esto es 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
¿Y sabes lo que mata el jazz?Alguien parado sobre tu hombro diciendo "escribe la prueba primero".¿ Por qué no hay pruebas todavía?
Porque*la forma sigue siendo fluida.*Imagínate que eres escultor.
Tienes un bloque de mármol y una vaga idea: "Quiero tallar un pájaro".
Define su envergadura.Especifique el ángulo de cada pluma.
Ahora esculpe esas especificaciones".*Así... no es como se tallan los pájaros. (O el software se escribe, como resulta.)*Escultores reales
Primero desbaratar la forma.Ellos dibujan.
Hacen maquetas.*Cortan grandes pedazos de piedra para encontrar la forma general.*Sólo después refinan los detalles.
El código es el mismo.
Este código es
privado.
Es una conversación conmigo misma.
Así es como me doy cuenta de lo que significa "trabajar".
activamente perjudiciales.
Porque cada vez que me doy cuenta de "oh espera, todo este enfoque está mal", tendría que reescribir las pruebas también.
Eso no es rigor.
La libertad de fallar rápidamente
# 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 es sobre
// 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);
}
velocidad de aprendizaje.
Necesito descubrir que la biblioteca de terceros que pensé usar... no es tan publicitada.*Necesito darme cuenta que el problema que estoy resolviendo no es el problema.deberesolverlo.*Las pruebas ralentizan esto.
No porque las pruebas sean lentas, sino porque
Entonces estoy emocionalmente invertido en lo incorrecto.
Es mejor fallar rápido, en privado, sin pruebas para mantener y sin ego para proteger.Lo que significa "trabajar" en la fase 1Significa: "Yo lo hice.
Le di algunas entradas.
# 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
)
Produjo productos que parecen razonables.
// 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();
Tengo un 60% de confianza en que entiendo el problema ahora".
Eso es.**No hay casos de ventaja.**No hay manejo de errores.
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
*Sólo un pico.*Una prueba de concepto.
Un sketch.Fase 2: Hacerlo bonito (el refinamiento)
Vale, ahora entiendo el problema.Tengo algo que funciona, al menos para el camino feliz.
Ahora puedo empezar a preocuparme por la calidad.
Ejemplo de Python:*C# Ejemplo:*Mejor nombre.
Anotaciones de tipo.*Documentación XML.*Implementación de LINQ más limpia.
Alinearse con el espectro
Ahora que sé lo que estoy construyendo, vuelvo a los requisitos originales y me aseguro de que estoy resolviendo el problema.
derecha
aproblema.
A menudo esto revela lagunas:
"Querían que esto manejara los números negativos de manera diferente".
experiencia.**Python:**C#:
Mejor nombre.
¿ Por qué todavía no hay pruebas?
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 estoy manejando constantemente.
Tengo una REPL abierta.
Porque
# 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]
Todavía estoy descubriendo lo que vale la pena probar.
En la Fase 1, no sabía si esta función existiría.En la Fase 2, estoy descubriendo:"El factor escala no puede ser negativo.
"Si la lista está vacía, deberíamos volver vacía, no Ninguno.""Necesitamos manejar los valores no numéricos con gracia".
Estos puntos de vista
emergen de la refactorización.
No son obvios por adelantado.
El código está limpio.*La API tiene sentido.*He cubierto los estuches del borde.
Ahora escribo pruebas.
Muchas pruebas.
Pruebas tan cercanas a la cobertura completa como sea posible.¿Por qué ahora?
Porque
las pruebas son un mecanismo de compartir.Ya no son para mí.
Ya entiendo este código íntimamente.
El oleoducto CI
(que necesita evitar regresiones)
Encargados del futuro
Las pruebas son las siguientes:capa de presentación
para mi implementación.Documentan:
¿Qué hace este código?Qué entradas son válidasQué productos esperar
¿Qué suposiciones estoy haciendo?
Cobertura completa, sin compromisos
Modificar esta función con confianza
Pruebas como documentación
documentación ejecutable.
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
**Pero tampoco escribo pruebas antes de que el código esté listo para compartir.**Las fases son las siguientes:
Exploración privada(solo yo, sin pruebas)
Refinamiento privado
(solo yo, todavía no hay pruebas) |-----|-------------| Participación del público (pruebas, CI, revisión de código, los nueve yardas enteros) Esto protege tanto mi creatividady | La cordura de mis compañeros. Por qué esto protege la creatividad
¡Es verdad!Pero sólo si ya sabes lo que estás construyendo.
Cuando estoy en la Fase 1, no necesito confianza para refactorizar.
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
Necesito permiso para tirar todo por la borda.
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
Las pruebas crean deuda psicológica.
Incluso si están probando la cosa equivocada.Aplazando las pruebas hasta la fase 3, mantengo las fases 1 y 2
psicológicamente barato.
Puedo:
Descubre el problema que soyEn realidadresolver el problema
Así es como funciona la creatividad.
// 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 }));
}
Usted bosqueja flojamente primero.
No empiezas con los detalles.
// 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);
}
}
Cómo encaja esto con Agile y XP (y dónde se desvía)
esto no es "prueba de desarrollo posterior".
Esto es
para añadir pruebas unitarias es crítico.
Las prácticas de XP que mantengo
Cada característica pasa por CI antes de fusionarse
El refinamiento colaborativo mejora el diseñoDiseño sencillo
La fase 2 es
Refactorización
Fase 2 es tiempo de refactorización dedicadoLimpiar el códigoantes
**Los ensayos protegen la refactorización (en la fase 3+)**Donde me desvío de la estricta TDD
TDD dice:"Primero las pruebas.
Siempre.Red-Green-Refactor es la única manera".
Yo digo:"Pruebas cuando sabes lo que estás probando.
Explore-Refine-Lock es más natural".
TDD Tres fasesLa prueba define la interfaz La exploración define la interfaz
Prueba primero para todo Prueba en el momento adecuado para cada cosa
Momento adecuado (etapa 3):
El mismo resultado de calidad.*La mitad del churn.*Esto es ágil.
"Responda a cambiar siguiendo un plan."La TDD puede convertirse en su propia forma de seguimiento de planes.
Una vez que has escrito pruebas, estás comprometida psicológicamente con ese diseño.
Esto esmás
Enfoques de contraste: Un ejemplo de C#
¿Te fijas en el churn?
Tres reescrituras de prueba a medida que el diseño evolucionó.
Recuerde los valores:
Fase 1 llega al software de trabajo
rápido
Colaborar cuando ayuda (Fase 2-3), explorar solo cuando no lo hace (Fase 1)
La fase 2 se alinea con la especificación
después
Porque:
Aún no conocías los casos del borde.Descubriste un mejor diseño de APITe diste cuenta de que la función no era necesaria.
Cada prueba que escribas en la Fase 1 es probablemente un esfuerzo desperdiciado.
uno
conjunto de pruebas en la Fase 3, cuando realmente sabes lo que estás construyendo, que escribir-reescribir-reescribir durante la exploración.La promesa TDD vs. la realidadTDD promete:
"Write code that solves the specification.
Don't worry about perfection yet.
Just get it working."
"¡Las pruebas impulsan tu diseño!
Ahora estoy reescribiendo tanto las pruebas como el código".**Eso no es diseño.**Eso es.
Golpeando.Mi enfoque:
Diseño a través de la implementación, luego bloquearlo con pruebas.
"Sin pruebas"*"Enviar y rezar"*Para cuando termine con la Fase 3, mi código tiene:
Cobertura completa de los ensayosCasos de borde manejados
Borrar el diseño de APIDocumentación (por medio de pruebas)
Exactamente lo mismo que TDD.
La diferencia es
La regla es simple:
- flake8 (style checking)
- pylint (code quality)
- mypy (type checking)
- black (formatting)
- radon (complexity analysis)
**Ningún código deja la Fase 2 sin la Fase 3.**Yo no:
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
Código de commit no probadoAbrir PRs sin pruebasFusionar a la principal sin que pase IC
Esta no es una idea nueva.Es como el trabajo creativo siempre ha funcionado:
Los pintores no comienzan con las pinceladas finales.*Ellos:*Sketchla composición (fase 1)
Pintura*las capas (Fase 2)*Barniz
# 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)
"""
)
proteger y presentar (Fase 3)
Ellos:Proyectodesordenado (Fase 1)
**para la durabilidad (Fase 3)**El esmalte es crucial.
Cómo se hace este mapa para el oleoducto DiSE Codegen*Aquí es donde esto se pone interesante:*aplicada
esta filosofía en el sistema DiSE (Directed Synthetic Evolution).
Generador
No hay optimización prematura
Inténtalo de nuevo con mayor creatividad (temperatura más 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
Escalá a un modelo más poderoso
*Exactamente.*Fase 1.
No se han escrito pruebas durante esta fase.El código es privado para el proceso de generación.
Fase 2 en diSE: la fase de optimizaciónUna vez que el código pasa la ejecución básica, DiSE se mueve aoptimización.
(3 iteraciones por defecto)
Esto esExactamente.
El código sigue siendo privado (todavía no está en el registro), pero lo estamos puliendo:Mejores nombres
Escribir sugerenciasEstructura más limpia
Menor complejidad
Mejor rendimientoLa forma ya no es fluida.
Sabemos lo que estamos construyendo.
pruebas de unidades formales.
Los ensayos cubren:*Ejemplos de especificación (corrección)*Casos de borde descubiertos durante la Fase 2
Características de rendimiento que se midieron*Entonces,*sólo si se aprueban los ensayos
, el código es:
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]
Agregado a laregistro de nodos:
en futuros flujos de trabajo
calificaciones de calidad
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.