You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/content/articles/legacy-code-testing-characterization-tests-seams/index.mdx
+20-18Lines changed: 20 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,11 +18,11 @@ Here's the paradox every developer faces with legacy code: you can't refactor sa
18
18
19
19
The code is "untestable." Except it isn't.
20
20
21
-
Two techniques unlock almost any legacy codebase: __characterization tests__ and __seam identification__. You don't need to understand the code to test it, and you don't need to refactor before you can write your first test. These techniques form the foundation that makes everything else possible.
21
+
<Highlighter>Two techniques unlock almost any legacy codebase: __characterization tests__ and __seam identification__. You don't need to understand the code to test it, and you don't need to refactor before you can write your first test.</Highlighter> These techniques form the foundation that makes everything else possible.
22
22
23
23
## Characterization Tests: Document Before You Judge
24
24
25
-
Traditional unit tests verify that code does what it __should__ do — you write a test based on a specification, and the test fails if the code doesn't match. Characterization tests flip this: they capture what the code __actually__ does, regardless of intent. You're not testing against a spec; you're documenting observed behavior.
25
+
Traditional unit tests verify that code does what it _should_ do — you write a test based on a specification, and the test fails if the code doesn't match. Characterization tests flip this: they capture what the code _actually_ does, regardless of intent. You're not testing against a spec; you're documenting observed behavior.
26
26
27
27
The distinction matters for legacy code. You don't have a spec. The original authors are gone. The code has undocumented edge cases, implicit business rules buried in conditionals, and behaviors that might be bugs or might be features — you can't tell. Characterization tests don't try to answer "is this correct?" They answer "what does this do?" and lock it in.
28
28
@@ -66,26 +66,28 @@ Consider a method that sends emails. The email-sending code is deep inside a 500
66
66
### The Four Seam Types
67
67
68
68
<List
69
-
variant="check-icons-list"
69
+
variant="numbered-with-background-list"
70
70
items={[
71
71
{
72
72
lead: 'Object seams',
73
-
text: 'are the most common and usually the cleanest. You pass a dependency through a constructor or method parameter, and the enabling point is the call site where you can pass a different implementation. This is the foundation of dependency injection. Most languages and frameworks have idiomatic ways to create object seams:',
73
+
text: 'The most common and usually the cleanest. You pass a dependency through a constructor or method parameter, and the enabling point is the call site where you can pass a different implementation. This is the foundation of dependency injection. Most languages and frameworks have idiomatic ways to create object seams:',
74
74
},
75
75
]}
76
76
/>
77
77
78
78
<Table
79
-
variant="vertical-column-delineation-table"
79
+
variant="grid-and-accent-header-table"
80
+
fullWidth={false}
80
81
content={{
82
+
figure: 'Object seam techniques by language and framework.',
td: ['Default arguments in <span class="font-mono">initialize</span>'],
89
91
},
90
92
{
91
93
th: 'C# / Java',
@@ -97,40 +99,40 @@ Consider a method that sends emails. The email-sending code is deep inside a 500
97
99
},
98
100
{
99
101
th: 'TypeScript',
100
-
td: ['Optional parameters with nullish coalescing (`??`)'],
102
+
td: ['Optional parameters with nullish coalescing (<span class="font-mono">??</span>)'],
101
103
},
102
104
{
103
105
th: 'Spring Boot',
104
-
td: ['`@Autowired` with test `@Configuration` bindings'],
106
+
td: ['<span class="font-mono">@Autowired</span> with test <span class="font-mono">@Configuration</span> bindings'],
105
107
},
106
108
{
107
109
th: 'Laravel',
108
-
td: ['Service container with `$this->app->bind()` in tests'],
110
+
td: ['Service container with <span class="font-mono">$this->app->bind()</span> in tests'],
109
111
},
110
112
{
111
113
th: '.NET',
112
-
td: ['`IServiceCollection` with test service registration'],
114
+
td: ['<span class="font-mono">IServiceCollection</span> with test service registration'],
113
115
},
114
116
],
115
117
},
116
-
figure: 'Object seam techniques by language and framework.',
117
118
}}
118
119
/>
119
120
120
121
<List
121
-
variant="check-icons-list"
122
+
variant="numbered-with-background-list"
123
+
startNumber={2}
122
124
items={[
123
125
{
124
126
lead: 'Link seams',
125
-
text: 'operate at the module level. In Ruby, you can use `stub_const` to replace a class entirely. In TypeScript/JavaScript, Jest\'s `jest.mock()` intercepts imports. The enabling point is the test setup. Link seams are powerful but fragile — they couple tests to implementation details like class names.',
127
+
text: 'These operate at the module level. In Ruby, you can use <span class="font-mono">stub_const</span> to replace a class entirely. In TypeScript/JavaScript, Jest\'s <span class="font-mono">jest.mock()</span> intercepts imports. The enabling point is the test setup. Link seams are powerful but fragile — they couple tests to implementation details like class names.',
126
128
},
127
129
{
128
130
lead: 'Subclass seams',
129
-
text: 'work by extracting behavior into a protected method, then overriding it in a test subclass. This technique is underrated for legacy code because it requires minimal changes — you extract one line into a method, and suddenly you have a seam.',
131
+
text: 'Works by extracting behavior into a protected method, then overriding it in a test subclass. This technique is underrated for legacy code because it requires minimal changes — you extract one line into a method, and suddenly you have a seam.',
130
132
},
131
133
{
132
134
lead: 'Preprocessor seams',
133
-
text: 'apply anywhere you use environment-based branching. Rails\'`Rails.env.test?`, Laravel\'s `app()->environment(\'testing\')`, and Node\'s `process.env.NODE_ENV === \'test\'` are all effectively preprocessor seams. Use them sparingly — they litter production code with test concerns.',
135
+
text: 'Applies anywhere you use environment-based branching. Rails\'<span class="font-mono">Rails.env.test?</span>, Laravel\'s <span class="font-mono">app()->environment(\'testing\')</span>, and Node\'s <span class="font-mono">process.env.NODE_ENV === \'test\'</span> are all effectively preprocessor seams. Use them sparingly — they litter production code with test concerns.',
134
136
},
135
137
]}
136
138
/>
@@ -178,8 +180,6 @@ The workflow looks like this: First, identify the behavior you need to protect.
178
180
179
181
With both in place, you can isolate and test without understanding the full system. The code is no longer untestable — it's testable through observation and substitution.
180
182
181
-
This is the foundation. Deeper techniques — Extract and Override for quick dependency breaking, Parameterize Constructor for clean DI patterns, Strangler Fig for system-level migration — all build on characterization tests and seams. But start here. Get your first characterization test passing. Find your first seam. The rest follows.
@@ -192,10 +192,12 @@ This is the foundation. Deeper techniques — Extract and Override for quick dep
192
192
]}
193
193
/>
194
194
195
+
This is the foundation. Deeper techniques — Extract and Override for quick dependency breaking, Parameterize Constructor for clean DI patterns, Strangler Fig for system-level migration — all build on characterization tests and seams. But start here. Get your first characterization test passing. Find your first seam. The rest follows.
196
+
195
197
## Conclusion
196
198
197
199
The myth of "untestable" code usually means "code that's hard to test with conventional techniques." Characterization tests and seams change the equation entirely — they let you observe, document, and isolate without first having to understand every line.
198
200
199
201
Start with characterization tests. Run the code, capture what happens, lock it down. Don't judge whether the behavior is correct — just document it. Then find seams: the constructor parameters, the class methods, the environment flags that let you substitute behavior without editing the code you're protecting.
200
202
201
-
These foundations enable everything else: dependency breaking, incremental extraction, system-level migration. But they're also sufficient on their own to turn "untestable" into testable. The question isn't __can__ you test legacy code — it's whether the investment is worth it for code that may never change.
203
+
These foundations enable everything else: dependency breaking, incremental extraction, system-level migration. But they're also sufficient on their own to turn "untestable" into testable. The question isn't _can_ you test legacy code — it's whether the investment is worth it for code that may never change.
0 commit comments