Skip to content

Commit bc52867

Browse files
committed
List and table styles to legacy-code-testing-characterization-tests-seams article
1 parent 1ef370b commit bc52867

2 files changed

Lines changed: 49 additions & 38 deletions

File tree

  • src/content/articles/legacy-code-testing-characterization-tests-seams

‎src/content/articles/legacy-code-testing-characterization-tests-seams/index.mdx‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ Here's the paradox every developer faces with legacy code: you can't refactor sa
1818

1919
The code is "untestable." Except it isn't.
2020

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.
2222

2323
## Characterization Tests: Document Before You Judge
2424

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.
2626

2727
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.
2828

@@ -66,26 +66,28 @@ Consider a method that sends emails. The email-sending code is deep inside a 500
6666
### The Four Seam Types
6767

6868
<List
69-
variant="check-icons-list"
69+
variant="numbered-with-background-list"
7070
items={[
7171
{
7272
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:',
7474
},
7575
]}
7676
/>
7777

7878
<Table
79-
variant="vertical-column-delineation-table"
79+
variant="grid-and-accent-header-table"
80+
fullWidth={false}
8081
content={{
82+
figure: 'Object seam techniques by language and framework.',
8183
thead: {
8284
th: ['Language/Framework', 'Object Seam Technique'],
8385
},
8486
tbody: {
8587
tr: [
8688
{
8789
th: 'Ruby',
88-
td: ['Default arguments in `initialize`'],
90+
td: ['Default arguments in <span class="font-mono">initialize</span>'],
8991
},
9092
{
9193
th: 'C# / Java',
@@ -97,40 +99,40 @@ Consider a method that sends emails. The email-sending code is deep inside a 500
9799
},
98100
{
99101
th: 'TypeScript',
100-
td: ['Optional parameters with nullish coalescing (`??`)'],
102+
td: ['Optional parameters with nullish coalescing (<span class="font-mono">??</span>)'],
101103
},
102104
{
103105
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'],
105107
},
106108
{
107109
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'],
109111
},
110112
{
111113
th: '.NET',
112-
td: ['`IServiceCollection` with test service registration'],
114+
td: ['<span class="font-mono">IServiceCollection</span> with test service registration'],
113115
},
114116
],
115117
},
116-
figure: 'Object seam techniques by language and framework.',
117118
}}
118119
/>
119120

120121
<List
121-
variant="check-icons-list"
122+
variant="numbered-with-background-list"
123+
startNumber={2}
122124
items={[
123125
{
124126
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.',
126128
},
127129
{
128130
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.',
130132
},
131133
{
132134
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.',
134136
},
135137
]}
136138
/>
@@ -178,8 +180,6 @@ The workflow looks like this: First, identify the behavior you need to protect.
178180

179181
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.
180182

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.
182-
183183
<Download
184184
resource="legacy-code-testing-characterization-tests-seams"
185185
title="Adding Tests to Untestable Legacy Code"
@@ -192,10 +192,12 @@ This is the foundation. Deeper techniques — Extract and Override for quick dep
192192
]}
193193
/>
194194

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+
195197
## Conclusion
196198

197199
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.
198200

199201
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.
200202

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

Comments
 (0)