π Translation: Translated from Korean.
I’ll translate this Korean blog post to English while preserving technical terms, code blocks, and maintaining SEO optimization. Let me start the translation:
TDD in Practice: Lessons Learned from Testing TypeScript with vitest
Executive Summary
- Transition: From no-test development β TDD (Test-Driven Development)
- Tools: vitest + TypeScript (10x faster than Jest)
- Results: 75 tests written, 90% faster bug detection, refactoring confidence gained
- Real Project: WordPress blog automation CLI tool (production-ready)
1. Why I Started TDD
In the early stages of the project, I developed without test code.
“Let’s build it fast first, add tests later”
But reality was different.
The Pain of Test-less Development
Problem 1: Refactoring Fear
// β Don't know what broke after refactoring
function validateTranslation(original, translated) {
// 100 lines of complex logic...
// Did my changes work properly? π€
}
Problem 2: Delayed Bug Detection
Timeline Event
Day 1 Feature implementation complete
Day 3 Working on other features...
Day 7 User discovers bug "Translation looks weird"
Day 8 Start finding root cause (where did it break?)
Day 9 Debugging complete (total 2 days spent)
Problem 3: Missing Edge Cases
// β Didn't think of these cases
validateTranslation('', ''); // empty string
validateTranslation('νκΈ', undefined); // undefined
validateTranslation(null, 'English'); // null
Decision: “I can’t develop like this anymore. Let’s learn TDD.”
2. Why I Chose vitest
There are many TypeScript testing tools. Why vitest?
2.1 Comparing Major Testing Frameworks
| Tool | Speed | ESM Support | TypeScript | Setup Complexity |
|---|---|---|---|---|
| vitest | β‘β‘β‘ (10x) | β Native | β Perfect | π’ Simple |
| Jest | π’ Slow | β οΈ Experimental | β Good | π‘ Moderate |
| Mocha | π’ Slow | β None | β οΈ Manual | π΄ Complex |
2.2 vitest’s Decisive Advantages
1. Overwhelming Speed
# Jest
β 75 tests in 8.5s
# vitest
β 75 tests in 0.8s (10x faster!)
2. Native ESM Support
// β
vitest: Just works
import { validateTranslation } from './validation.js';
// β Jest: Configuration hell
// Add "type": "module" to package.json
// babel config, transform settings...
3. Perfect TypeScript Support
// β
Automatic type inference
import { describe, it, expect } from 'vitest';
describe('Validation', () => {
it('should validate', () => {
const result = validateTranslation(/* ... */);
// result type automatically inferred!
});
});
4. Jest-Compatible API
// If you have Jest experience, use it immediately
describe, it, expect, beforeEach, afterEach
vi.mock(), vi.spyOn() // Same as Jest's jest.mock()
3. vitest Setup and First Test
3.1 Installation and Configuration
# 1. Install vitest
pnpm add -D vitest
# 2. Add scripts to package.json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui"
}
}
vitest.config.ts (optional):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // Use describe, it, expect without imports
environment: 'node',
},
});
3.2 Writing Your First Test
Test Target (packages/core/src/validation.ts):
export function validateBasics(content: string): boolean {
if (!content || content.trim().length === 0) {
return false;
}
return true;
}
Test Code (packages/core/src/validation.test.ts):
import { describe, it, expect } from 'vitest';
import { validateBasics } from './validation';
describe('validateBasics', () => {
it('should return false for empty content', () => {
const result = validateBasics('');
expect(result).toBe(false);
});
it('should return false for whitespace only', () => {
const result = validateBasics(' ');
expect(result).toBe(false);
});
it('should return true for valid content', () => {
const result = validateBasics('Hello World');
expect(result).toBe(true);
});
});
Execution:
pnpm test
# Result
β src/validation.test.ts (3 tests) 5ms
β validateBasics (3)
β should return false for empty content
β should return false for whitespace only
β should return true for valid content
Test Files 1 passed (1)
Tests 3 passed (3)
4. TDD Workflow in Practice: Red-Green-Refactor
The process of developing the validateTranslation function using TDD in a real project.
4.1 Red: Write a Failing Test
Requirement: “Translated content must have line count within 50-150% range of the original”
// validation.test.ts
describe('validateLineCount', () => {
it('should pass if line count is within 50-150% range', () => {
const original = 'Line 1\nLine 2\nLine 3\nLine 4'; // 4 lines
const translated = 'Line 1\nLine 2\nLine 3\nLine 4\nLine 5'; // 5 lines (125%)
const result = validateLineCount(original, translated);
expect(result.isValid).toBe(true);
expect(result.issues).toHaveLength(0);
});
it('should fail if line count is too low (<50%)', () => {
const original = 'Line 1\nLine 2\nLine 3\nLine 4'; // 4 lines
const translated = 'Line 1'; // 1 line (25%)
const result = validateLineCount(original, translated);
expect(result.isValid).toBe(false);
expect(result.issues).toContainEqual({
severity: 'error',
message: expect.stringContaining('Line count too low')
});
});
});
Execution Result:
β validateLineCount > should pass if line count is within 50-150% range
ReferenceError: validateLineCount is not defined
π΄ Red: Test fails (function doesn’t exist)
4.2 Green: Minimal Code to Pass the Test
// validation.ts
export function validateLineCount(
original: string,
translated: string
): ValidationResult {
const originalLines = original.split('\n').length;
const translatedLines = translated.split('\n').length;
const ratio = translatedLines / originalLines;
const issues: ValidationIssue[] = [];
if (ratio < 0.5) {
issues.push({
severity: 'error',
message: `Line count too low: ${translatedLines} lines (${(ratio * 100).toFixed(1)}% of original ${originalLines} lines)`
});
}
if (ratio > 1.5) {
issues.push({
severity: 'error',
message: `Line count too high: ${translatedLines} lines (${(ratio * 100).toFixed(1)}% of original ${originalLines} lines)`
});
}
return {
isValid: issues.length === 0,
issues
};
}
Execution Result:
β validateLineCount (2 tests) 3ms
β should pass if line count is within 50-150% range
β should fail if line count is too low (<50%)
π’ Green: Test passes!
4.3 Refactor: Code Improvement
// validation.ts (refactored)
const LINE_COUNT_MIN_RATIO = 0.5;
const LINE_COUNT_MAX_RATIO = 1.5;
export function validateLineCount(
original: string,
translated: string
): ValidationResult {
const originalLines = countLines(original);
const translatedLines = countLines(translated);
const ratio = translatedLines / originalLines;
const issues = [
...checkLineCountTooLow(ratio, originalLines, translatedLines),
...checkLineCountTooHigh(ratio, originalLines, translatedLines),
];
return {
isValid: issues.length === 0,
issues
};
}
// Helper functions
function countLines(content: string): number {
return content.split('\n').length;
}
function checkLineCountTooLow(
ratio: number,
originalLines: number,
translatedLines: number
): ValidationIssue[] {
if (ratio >= LINE_COUNT_MIN_RATIO) return [];
return [{
severity: 'error',
message: `Line count too low: ${translatedLines} lines (${(ratio * 100).toFixed(1)}% of original ${originalLines} lines)`
}];
}
function checkLineCountTooHigh(
ratio: number,
originalLines: number,
translatedLines: number
): ValidationIssue[] {
if (ratio <= LINE_COUNT_MAX_RATIO) return [];
return [{
severity: 'error',
message: `Line count too high: ${translatedLines} lines (${(ratio * 100).toFixed(1)}% of original ${originalLines} lines)`
}];
}
Re-run Tests:
β validateLineCount (2 tests) 2ms
β Refactor: Tests still pass, code is cleaner!
5. Advanced Testing Patterns
5.1 Testing Async Functions
// translator.ts
export async function translatePost(
content: string
): Promise<string> {
const response = await fetch('https://api.claude.ai/translate', {
method: 'POST',
body: JSON.stringify({ content })
});
return response.json();
}
// translator.test.ts
import { describe, it, expect, vi } from 'vitest';
describe('translatePost', () => {
it('should translate content via API', async () => {
// β
Use async/await
const result = await translatePost('μλ
νμΈμ');
expect(result).toContain('Hello');
});
});
5.2 Mocking
// translator.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
// β
Mock fetch
global.fetch = vi.fn();
describe('translatePost', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should call API with correct params', async () => {
// Setup mock
(fetch as any).mockResolvedValue({
json: async () => 'Translated text'
});
await translatePost('Original text');
// Verify API call
expect(fetch).toHaveBeenCalledWith(
'https://api.claude.ai/translate',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ content: 'Original text' })
})
);
});
it('should handle API errors', async () => {
// Mock API error
(fetch as any).mockRejectedValue(new Error('Network error'));
await expect(translatePost('text')).rejects.toThrow('Network error');
});
});
5.3 Testing Exception Handling
describe('validateTranslation', () => {
it('should throw error for invalid input', () => {
expect(() => {
validateTranslation(null as any, 'valid');
}).toThrow('Original content is required');
});
it('should return error for empty content', async () => {
const result = await validateTranslation('', '');
expect(result.isValid).toBe(false);
expect(result.issues).toContainEqual({
severity: 'error',
message: 'Content is empty'
});
});
});
6. Coverage Measurement and Quality Management
6.1 Coverage Setup
# Install @vitest/coverage-v8
pnpm add -D @vitest/coverage-v8
# Run coverage
pnpm test --coverage
Result:
% Coverage report from v8
-------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-------------------|---------|----------|---------|---------|
All files | 94.23 | 89.47 | 92.85 | 94.23 |
validation.ts | 100 | 100 | 100 | 100 |
translator.ts | 88.88 | 83.33 | 85.71 | 88.88 |
markdown.ts | 95.45 | 91.66 | 93.75 | 95.45 |
-------------------|---------|----------|---------|---------|
6.2 Coverage Goals
Recommended Thresholds:
- Statements: β₯80%
- Branches: β₯75%
- Functions: β₯80%
- Lines: β₯80%
Project Actual Achievement:
- β Statements: 94.23%
- β Branches: 89.47%
- β Functions: 92.85%
- β Lines: 94.23%
6.3 Finding Low Coverage Code
# Generate HTML report
pnpm test --coverage --reporter=html
# Open coverage/index.html
open coverage/index.html
Lines highlighted in red = untested code
7. Practical Tips and Pitfalls
Tip 1: Clear Test Names
// β Bad examples
it('works', () => { ... });
it('test 1', () => { ... });
// β
Good examples
it('should return false for empty content', () => { ... });
it('should throw error when API key is missing', () => { ... });
Tip 2: Test Isolation (beforeEach)
describe('WordPressClient', () => {
let client: WordPressClient;
// β
Create new instance before each test
beforeEach(() => {
client = new WordPressClient({
url: 'https://test.com',
username: 'test',
password: 'test'
});
});
it('should create post', async () => {
// client is in clean state
});
it('should update post', async () => {
// No impact from previous test
});
});
Tip 3: One Assertion per Test
// β Bad: Multiple assertions
it('should do everything', () => {
expect(result.isValid).toBe(true);
expect(result.issues).toHaveLength(0);
expect(result.metrics.lineCountDiff).toBe(0);
expect(result.metrics.codeBlocks).toBe(3);
// ... 10 more
});
// β
Good: Separate tests
it('should be valid', () => {
expect(result.isValid).toBe(true);
});
it('should have no issues', () => {
expect(result.issues).toHaveLength(0);
});
it('should preserve line count', () => {
expect(result.metrics.lineCountDiff).toBe(0);
});
Pitfall 1: Test Order Dependencies
// β Bad example
let sharedState = 0;
it('test 1', () => {
sharedState = 10;
});
it('test 2', () => {
// Assumes test 1 runs first (dangerous!)
expect(sharedState).toBe(10);
});
Solution: Each test should be independent (use beforeEach)
Pitfall 2: Too Many Mocks
// β Bad: Mock everything
vi.mock('./file1');
vi.mock('./file2');
vi.mock('./file3');
// ... 10 more
// β
Good: Mock only what's necessary (external APIs, file system, etc.)
vi.mock('node:fs');
global.fetch = vi.fn();
8. Measuring Real-World Results
8.1 Measurable Outcomes
| Metric | Before TDD | After TDD | Improvement |
|---|---|---|---|
| Bug Detection | 7 days later (user found) | Immediate (test fails) | 90% faster |
| Refactoring Time | 2 hours (anxious) | 30 min (confident) | 75% reduction |
| Debugging Time | Average 4 hours | Average 30 min | 87% reduction |
| Code Coverage | 0% | 94% | +94%p |
8.2 Real Cases
Case 1: Translation Validation Bug
Before TDD:
- Day 1: Translation feature implementation complete
- Day 5: User "Code blocks disappeared"
- Day 6: Start finding root cause
- Day 7: Fix complete (total 3 days spent)
After TDD:
- Day 1: Write test β Verify code block preservation
- Day 1: Implementation β Test passes
- 0 bugs (prevented by tests)
Case 2: Refactoring Confidence
Before TDD:
- "What will break if I modify this function?" π°
- Manual testing of entire app after changes (2 hours)
After TDD:
- "Run pnpm test" π
- 75 tests pass (30 seconds)
- Deploy with confidence
9. Conclusion
Key Takeaways
TDD Value:
- β Bug detection 90% faster (user discovery β immediate test failure detection)
- β Refactoring confidence (tests are safety net)
- β Documentation effect (tests = executable examples)
- β Design improvement (testable code = good design)
vitest Advantages:
- β‘ 10x faster than Jest
- π’ Perfect TypeScript support
- π¦ Native ESM
- π§ Simple setup
You Can Start Too
Minimal Start (10 minutes):
# 1. Install
pnpm add -D vitest
# 2. Write first test
echo 'import { describe, it, expect } from "vitest";
describe("My Function", () => {
it("should work", () => {
expect(1 + 1).toBe(2);
});
});' > src/example.test.ts
# 3. Run
pnpm vitest
# Done! You're now a TDD developer π
Next Steps
If you’ve mastered this pattern, check out these topics:
- Day 3 Preview: Practical Guide to API Error Handling (Timeout, Retry, Fallback)
- Related Topic: TypeScript Error Handling Best Practices
Final Thoughts
“Testing is not a waste of time, it’s an investment of time.”
It’s slow at first, but long-term you gain 10x faster development speed.
Developing without tests is like driving without a seatbelt.
Start safe and confident development with vitest + TDD.
Questions or feedback? Leave a comment!
Real Project Code: GitHub Repository – 75 tests, 94% coverage
Leave A Comment