ワンクリックで
characterization-testing
Create tests that describe what legacy code actually does (not what it should do) as safety net before refactoring
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Create tests that describe what legacy code actually does (not what it should do) as safety net before refactoring
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
Transform markdown notes into engaging technical blog posts for both programmers and non-technical readers
Systematic techniques for reading and understanding unfamiliar legacy code without documentation
Map dependencies and coupling in legacy codebase to understand what breaks when you change something and identify refactoring risks
Create detailed implementation plans with bite-sized tasks for engineers with zero codebase context
Creating documentation from legacy code when none exists, focusing on what future maintainers need to know
Maintain skills wiki health - check links, naming, cross-references, and coverage
| name | Characterization Testing |
| description | Create tests that describe what legacy code actually does (not what it should do) as safety net before refactoring |
| when_to_use | when working with legacy code that has no tests and unclear behavior, before attempting any changes or refactoring |
| version | 1.0.0 |
| languages | all |
Characterization tests capture current behavior of legacy code, warts and all. They're a safety net before refactoring, not a specification of correctness.
Core principle: Document what IS, not what SHOULD BE. Fix behavior later, after safety net exists.
This is NOT unit testing. Unit tests specify desired behavior. Characterization tests document actual behavior.
Use characterization testing when:
Don't use when:
NO REFACTORING WITHOUT CHARACTERIZATION TESTS FIRST
Refactoring without tests = gambling with production. Always create safety net first.
Choose smallest meaningful unit to characterize:
Start small. You can always expand coverage later.
Write test with unknown expectation:
test('processes user data', () => {
const result = processUserData({ name: 'John', age: 30 });
expect(result).toEqual(/* ??? what does it return? */);
});
Don't guess. Leave expectation blank or use placeholder.
Run the test. It will fail. Copy the actual output exactly:
$ npm test
FAIL: expected ???, received { fullName: 'John', isAdult: true, category: 'standard' }
This is the characterization: what the code actually does right now.
Update test with actual output:
test('processes user data', () => {
const result = processUserData({ name: 'John', age: 30 });
expect(result).toEqual({
fullName: 'John',
isAdult: true,
category: 'standard'
});
});
Run test again → should pass. You've characterized the behavior.
Find weird inputs and capture outputs:
test('handles missing age', () => {
const result = processUserData({ name: 'John' });
// Run test, see what happens, lock it in
expect(result).toEqual({
fullName: 'John',
isAdult: false,
category: 'unknown'
});
});
test('handles negative age (current behavior - BUG)', () => {
const result = processUserData({ name: 'John', age: -5 });
// This is wrong but it's what code does now
expect(result).toEqual({
fullName: 'John',
isAdult: true, // BUG: negative age treated as adult!
category: 'standard'
});
});
test('handles empty name', () => {
const result = processUserData({ name: '', age: 30 });
expect(result).toEqual({
fullName: '',
isAdult: true,
category: 'standard'
});
});
test('handles null input', () => {
// Might throw error, might return null - capture what happens
expect(() => processUserData(null)).toThrow('Cannot read property');
});
Key insight: You're documenting bugs, not fixing them. Tests show what code does, including incorrect behavior.
Mark tests for known bugs:
test.skip('FIXME: should reject negative age', () => {
// This is what SHOULD happen (not what happens now)
expect(() => processUserData({ name: 'John', age: -5 }))
.toThrow('Invalid age: must be non-negative');
});
test('handles negative age (CURRENT BEHAVIOR - BUG)', () => {
// This is what ACTUALLY happens now
const result = processUserData({ name: 'John', age: -5 });
expect(result.isAdult).toBe(true); // Wrong! But it's current behavior
});
Why both tests?
.skip test shows desired behavior (for future)Ensure main execution paths covered:
Not 100% code coverage. Focus on behavior coverage: scenarios that matter.
Legacy code we need to refactor:
function calculateDiscount(user, cart) {
let total = 0;
for (let i = 0; i < cart.items.length; i++) {
total += cart.items[i].price * cart.items[i].quantity;
}
if (user.isPremium) {
total = total * 0.9;
}
if (cart.items.length > 5) {
total = total * 0.95;
}
return Math.round(total * 100) / 100;
}
Characterization tests:
describe('calculateDiscount - characterization', () => {
test('standard user, small cart', () => {
const user = { isPremium: false };
const cart = {
items: [
{ price: 10, quantity: 2 },
{ price: 5, quantity: 1 }
]
};
const result = calculateDiscount(user, cart);
expect(result).toBe(25); // 10*2 + 5*1 = 25
});
test('premium user gets 10% discount', () => {
const user = { isPremium: true };
const cart = { items: [{ price: 100, quantity: 1 }] };
const result = calculateDiscount(user, cart);
expect(result).toBe(90); // 100 * 0.9 = 90
});
test('more than 5 items gets additional 5% discount', () => {
const user = { isPremium: false };
const cart = {
items: Array(6).fill({ price: 10, quantity: 1 })
};
const result = calculateDiscount(user, cart);
expect(result).toBe(57); // 60 * 0.95 = 57
});
test('premium + bulk discounts stack (CURRENT BEHAVIOR)', () => {
const user = { isPremium: true };
const cart = {
items: Array(6).fill({ price: 10, quantity: 1 })
};
const result = calculateDiscount(user, cart);
expect(result).toBe(51.3); // 60 * 0.9 * 0.95 = 51.3
});
test('empty cart returns 0', () => {
const user = { isPremium: false };
const cart = { items: [] };
const result = calculateDiscount(user, cart);
expect(result).toBe(0);
});
test('missing isPremium field (CURRENT BEHAVIOR - BUG?)', () => {
const user = {}; // no isPremium field
const cart = { items: [{ price: 100, quantity: 1 }] };
const result = calculateDiscount(user, cart);
expect(result).toBe(100); // Falsy check treats missing as non-premium
});
test('null user throws error', () => {
const cart = { items: [{ price: 100, quantity: 1 }] };
expect(() => calculateDiscount(null, cart))
.toThrow("Cannot read property 'isPremium' of null");
});
});
Now safe to refactor! If refactoring breaks these tests, you've changed behavior (maybe accidentally).
Bad:
test('negative price should be rejected', () => {
expect(() => calculateDiscount(user, { items: [{ price: -10, quantity: 1 }] }))
.toThrow('Invalid price');
});
This is what SHOULD happen, not what DOES happen. You're writing specification, not characterization.
Good:
test('negative price (CURRENT BEHAVIOR - BUG)', () => {
const user = { isPremium: false };
const cart = { items: [{ price: -10, quantity: 1 }] };
const result = calculateDiscount(user, cart);
expect(result).toBe(-10); // Bug: negative total! But this is current behavior
});
test.skip('FIXME: negative price should be rejected', () => {
// This is desired future behavior
expect(() => calculateDiscount(user, { items: [{ price: -10, quantity: 1 }] }))
.toThrow('Invalid price');
});
Bad:
1. Look at legacy code
2. "This is messy, let me clean it up"
3. Refactor
4. Add tests
Good:
1. Look at legacy code
2. Add characterization tests
3. Verify tests pass
4. Refactor with confidence
5. Tests still pass → safe refactoring
Bad:
test('calls database with correct params', () => {
const mockDB = jest.fn();
processUserData(mockDB, user);
expect(mockDB).toHaveBeenCalledWith('users', { id: 123 });
});
This tests interactions, not behavior. You don't know what the function returns.
Good:
test('processes user data from database', () => {
// Use real database or test database
const result = processUserData({ id: 123 });
expect(result).toEqual({ name: 'John', email: 'john@example.com' });
});
Characterization tests should test real behavior with real dependencies when possible.
Bad:
// I found this bug but I'm not going to test it because it's embarrassing
Good:
test('allows XSS in user input (CURRENT BEHAVIOR - SECURITY BUG)', () => {
const result = renderUserProfile({ name: '<script>alert("xss")</script>' });
expect(result).toContain('<script>alert("xss")</script>');
// Bug exists! But test documents it so we can fix it later
});
Document all bugs, especially security issues. Better to know than to be surprised.
| Excuse | Reality |
|---|---|
| "Code is too complex to test" | Characterization tests don't need full understanding. Capture behavior empirically. |
| "I'll refactor, then add tests" | Refactoring without tests = hoping you didn't break anything. Tests first. |
| "Tests will take too long" | Hours of characterization vs days of debugging production. Tests are faster. |
| "I know what the code should do" | Great! But what does it actually do? They might differ. |
| "I'll just be careful" | You will miss edge cases. Tests catch what you forget. |
| "Bugs are embarrassing to test" | Documented bugs can be fixed. Hidden bugs cause incidents. |
Now you have safety net. Next steps:
Characterization tests are temporary. They're scaffolding for refactoring, not permanent test suite.