Test Generation & Documentation
Tests and documentation are the foundation of maintainable software, yet they're often treated as afterthoughts. Claude Code excels at both tasks because it can analyze existing patterns, understand context, and apply consistent conventions across your codebase. In this lesson, you'll learn how to leverage Claude to generate comprehensive test suites and documentation that match your project's style.
Learning Objectives
- Identify untested code and generate test scaffolding that matches existing patterns
- Create edge case tests by analyzing code paths for error conditions and boundary values
- Generate integration tests for APIs, databases, and external services
- Set and achieve specific test coverage targets with Claude's guidance
- Generate JSDoc/TSDoc comments and comprehensive README files
- Apply the verification principle: tests validate Claude's work
The Testing Gap: Finding What's Missing
Before you can write tests, you need to know what's untested. Claude can analyze your codebase to identify coverage gaps and prioritize what needs testing.
Identifying Untested Code
The most direct approach is to ask Claude to compare your source files against your test files:
Find functions in src/services/NotificationsService.swift that are not covered by tests
Claude will read both the implementation and test files, then produce a report:
Based on my analysis:
UNTESTED FUNCTIONS:
- scheduleNotification(userId:message:delay:)
- cancelScheduledNotification(notificationId:)
- getUserNotificationPreferences(userId:)
PARTIALLY TESTED:
- sendNotification(userId:message:)
(only happy path tested, no error handling tests)
WELL COVERED:
- getNotificationHistory(userId:limit:)
- markAsRead(notificationId:)
This analysis gives you a clear roadmap for where to focus testing effort.
Ask Claude to prioritize by business criticality: "Which untested functions pose the highest risk?" Claude can analyze code complexity, external dependencies, and usage patterns to guide your testing strategy.
Generating Test Scaffolding
Once you know what needs testing, Claude can generate test scaffolding that matches your project's conventions.
Matching Existing Test Patterns
The key to useful test generation is pattern matching. Claude examines your existing tests to understand:
- Test framework: Jest, Mocha, XCTest, pytest, etc.
- File naming:
*.test.ts,*_test.go,test_*.py - Assertion style:
expect(),assert(),XCTAssertEqual() - Test organization:
describe/itblocks, class-based, function-based - Mock patterns: How you handle dependencies, external services, time
Here's an effective prompt:
Add tests for the NotificationsService. Follow the patterns used in existing test files.
Claude will:
- Read
NotificationsService.swift - Find related test files (e.g.,
tests/NotificationsServiceTests.swift) - Analyze test structure and conventions
- Generate new tests matching the established style
Claude reads the implementation
Claude examines the service code to understand:
- Function signatures and parameters
- Dependencies (database, external APIs, etc.)
- Error handling patterns
- State management
Claude analyzes existing tests
Claude reviews your test suite to identify:
- How mocks are created (
MockUserRepository,FakeTimeProvider) - Setup/teardown patterns
- Common test data fixtures
- Assertion conventions
Claude generates matching tests
The generated tests mirror your existing style:
class NotificationsServiceTests: XCTestCase {
var service: NotificationsService!
var mockUserRepo: MockUserRepository!
var mockScheduler: MockNotificationScheduler!
override func setUp() {
super.setUp()
mockUserRepo = MockUserRepository()
mockScheduler = MockNotificationScheduler()
service = NotificationsService(
userRepo: mockUserRepo,
scheduler: mockScheduler
)
}
func testScheduleNotificationWithValidUser() {
// Given
let userId = "user123"
mockUserRepo.stubbedUser = User(id: userId, name: "Test")
// When
let result = service.scheduleNotification(
userId: userId,
message: "Test notification",
delay: 3600
)
// Then
XCTAssertTrue(result.isSuccess)
XCTAssertEqual(mockScheduler.scheduledCount, 1)
}
}Edge Case Coverage: Beyond the Happy Path
The real value of Claude's test generation is identifying edge cases you might miss. Claude analyzes code paths to suggest tests for error conditions, boundary values, and unexpected inputs.
Prompting for Edge Cases
After generating basic tests, ask Claude to go deeper:
Add test cases for edge conditions in the notification service
Claude will analyze the code and suggest tests for:
Error Conditions:
- Network failures when sending notifications
- Database errors when fetching user preferences
- Invalid user IDs
- Authorization failures
Boundary Values:
- Empty message strings
- Maximum message length
- Negative delay values
- Delay exceeding system limits
State-Dependent Scenarios:
- Canceling an already-sent notification
- Scheduling notifications for deleted users
- Concurrent notification requests
- Rate limiting edge cases
Claude can't predict every edge case in your specific domain. Review generated tests and add business-specific scenarios Claude might not infer (e.g., regulatory requirements, compliance rules, industry-specific constraints).
Example Edge Case Tests
Here's what Claude might generate:
func testScheduleNotificationWithEmptyMessage() {
let result = service.scheduleNotification(
userId: "user123",
message: "",
delay: 3600
)
XCTAssertTrue(result.isFailure)
XCTAssertEqual(result.error, .invalidMessage)
}
func testScheduleNotificationWithNegativeDelay() {
let result = service.scheduleNotification(
userId: "user123",
message: "Test",
delay: -100
)
XCTAssertTrue(result.isFailure)
XCTAssertEqual(result.error, .invalidDelay)
}
func testScheduleNotificationForNonexistentUser() {
mockUserRepo.stubbedUser = nil
let result = service.scheduleNotification(
userId: "ghost-user",
message: "Test",
delay: 3600
)
XCTAssertTrue(result.isFailure)
XCTAssertEqual(result.error, .userNotFound)
}Running and Verifying Tests
Generated tests are only valuable if they pass. Always verify Claude's work by running the test suite.
The Verification Loop
Run the new tests and fix any failures
This single prompt triggers a powerful workflow:
- Claude runs the tests using your project's test command (
npm test,swift test,pytest, etc.) - Analyzes failures from the test output
- Identifies root causes (incorrect assertions, missing mocks, wrong test data)
- Fixes the tests or implementation as needed
- Re-runs until all tests pass
Initial test run reveals failures
FAIL src/services/__tests__/NotificationsService.test.ts
● scheduleNotification › should create scheduled notification
Expected: {"success": true}
Received: {"success": false, "error": "Missing scheduler configuration"}
Claude diagnoses the issue
Claude reads the error and identifies that the mock scheduler needs configuration that the test didn't provide.
Claude fixes the test
beforeEach(() => {
mockScheduler = new MockNotificationScheduler();
mockScheduler.configure({ maxDelay: 86400 }); // Add missing config
service = new NotificationsService(mockUserRepo, mockScheduler);
});Re-run confirms success
PASS src/services/__tests__/NotificationsService.test.ts
✓ scheduleNotification › should create scheduled notification (12ms)
This verification loop embodies the core principle: tests validate Claude's work. If the tests fail, Claude has clear, objective feedback to improve.
Integration Tests: Beyond Unit Tests
Unit tests verify individual functions in isolation. Integration tests verify that components work together correctly—database interactions, API endpoints, external services.
Testing API Endpoints
Integration tests for APIs typically involve:
- Setting up test databases or mocked backends
- Making HTTP requests to endpoints
- Validating response status, headers, and body
- Verifying database state changes
Prompt Claude with specific context:
Create integration tests for the POST /notifications endpoint. Use the patterns from tests/api/users.test.ts
Claude will generate tests that:
describe('POST /notifications', () => {
let app: Express;
let testDb: TestDatabase;
beforeAll(async () => {
testDb = await setupTestDatabase();
app = createApp(testDb);
});
afterAll(async () => {
await testDb.teardown();
});
it('should create notification with valid request', async () => {
const response = await request(app)
.post('/notifications')
.send({
userId: 'user123',
message: 'Test notification',
delay: 3600
})
.expect(201);
expect(response.body).toMatchObject({
notificationId: expect.any(String),
scheduledFor: expect.any(String)
});
// Verify database state
const notification = await testDb.notifications.findById(
response.body.notificationId
);
expect(notification).toBeDefined();
expect(notification.status).toBe('scheduled');
});
it('should return 400 for invalid user ID', async () => {
await request(app)
.post('/notifications')
.send({
userId: '',
message: 'Test',
delay: 3600
})
.expect(400);
});
});Mocking External Services
Integration tests often need to mock external dependencies (payment processors, email services, third-party APIs). Claude can generate mock implementations:
Add integration tests for the payment flow. Mock the Stripe API using the pattern from tests/mocks/StripeClient.ts
Claude will create tests with appropriate mocks:
describe('Payment Flow Integration', () => {
let mockStripe: MockStripeClient;
beforeEach(() => {
mockStripe = new MockStripeClient();
mockStripe.setSuccessMode(); // Simulate successful payments
});
it('should process payment and send notification', async () => {
const result = await processPayment({
amount: 5000,
userId: 'user123',
stripeClient: mockStripe
});
expect(result.success).toBe(true);
expect(mockStripe.charges).toHaveLength(1);
expect(mockStripe.charges[0].amount).toBe(5000);
});
it('should handle payment failures gracefully', async () => {
mockStripe.setFailureMode({ code: 'card_declined' });
const result = await processPayment({
amount: 5000,
userId: 'user123',
stripeClient: mockStripe
});
expect(result.success).toBe(false);
expect(result.error).toBe('card_declined');
});
});Setting Coverage Targets
Once you have tests, you can ask Claude to achieve specific coverage thresholds.
Coverage-Driven Test Generation
Add tests to achieve 90% line coverage for the NotificationsService
Claude will:
- Run coverage analysis (using
jest --coverage,pytest --cov, etc.) - Identify uncovered lines from the coverage report
- Generate tests targeting those lines
- Re-run coverage to verify the target is met
Start with realistic targets. Jumping from 40% to 95% coverage may not be practical. Aim for incremental improvements (60% → 75% → 85%) and focus on critical paths first.
Coverage Report Analysis
Claude can read coverage reports and prioritize:
File: NotificationsService.ts
Lines: 68.2% (45/66)
Functions: 75% (6/8)
Branches: 50% (8/16)
UNCOVERED LINES:
- Lines 34-38: Error handling for scheduler failure
- Lines 52-54: Retry logic for transient failures
- Line 67: Edge case for notification already sent
Claude then generates tests specifically targeting those uncovered paths.
Documentation Generation
Tests verify behavior; documentation explains intent. Claude can generate comprehensive documentation that matches your project's conventions.
Finding Undocumented Code
Find functions in src/services/ that are missing JSDoc comments
Claude scans your codebase and reports:
UNDOCUMENTED FUNCTIONS:
src/services/NotificationsService.ts:
- scheduleNotification (line 23)
- cancelScheduledNotification (line 45)
- getUserNotificationPreferences (line 67)
src/services/UserService.ts:
- updateUserProfile (line 89)
- deleteUserAccount (line 112)
Generating JSDoc Comments
Once you know what needs documentation, Claude can add comments:
Add JSDoc comments to undocumented functions in NotificationsService
Claude generates documentation that includes:
- Description: What the function does
- Parameters: Type, name, description
- Returns: Return type and meaning
- Throws: Possible error conditions
- Examples: Usage examples when helpful
Example output:
/**
* Schedules a notification to be sent to a user after a specified delay.
*
* This function validates the user exists, checks notification preferences,
* and queues the notification in the scheduler. If the user has disabled
* notifications, the function returns success but does not schedule.
*
* @param userId - The unique identifier of the user to notify
* @param message - The notification message content (max 500 characters)
* @param delay - Delay in seconds before sending (must be > 0)
* @returns A result indicating success or failure with error details
* @throws {UserNotFoundError} If the userId does not match any user
* @throws {InvalidDelayError} If delay is negative or exceeds max allowed
*
* @example
* ```typescript
* const result = await scheduleNotification(
* 'user123',
* 'Your order has shipped!',
* 3600 // Send in 1 hour
* );
* ```
*/
async function scheduleNotification(
userId: string,
message: string,
delay: number
): Promise<Result<NotificationId, NotificationError>> {
// Implementation...
}Improving Existing Documentation
For functions that already have basic comments, you can ask Claude to enhance them:
Improve documentation for NotificationsService with more context and examples
Claude will:
- Add missing parameter descriptions
- Include usage examples
- Document edge cases and error conditions
- Add cross-references to related functions
- Clarify ambiguous descriptions
| Before | After Enhancement |
|---|---|
| // Schedules a notification | Schedules a notification with delay, validates user preferences, handles errors gracefully |
| @param userId - User ID | @param userId - The unique identifier of the user (must exist in database) |
| @returns Result | @returns Success with notificationId, or failure with specific error code |
| (no examples) | Includes working code example with typical usage |
README Generation
Project README files are critical for onboarding and maintenance. Claude can generate comprehensive READMEs based on your codebase.
Generating a New README
Generate a README for this project based on the codebase structure and package.json
Claude will create a README that includes:
- Project name and description (from
package.json) - Installation instructions (dependencies, setup steps)
- Usage examples (based on entry points and common patterns)
- API documentation (key modules and their purpose)
- Development guide (build, test, lint commands)
- Project structure (directory layout explanation)
- Contributing guidelines (if applicable)
- License (from
LICENSEfile orpackage.json)
Updating an Existing README
For projects with outdated READMEs:
Update the README to reflect recent changes. The API endpoint structure has changed and we added a new authentication system.
Claude will:
- Read the existing README
- Analyze recent code changes
- Update relevant sections
- Preserve the existing tone and structure
- Add new sections as needed
Claude can also generate specialized documentation like CONTRIBUTING.md, ARCHITECTURE.md, or API.md. Just specify what you need: "Generate an ARCHITECTURE.md explaining the service layer design pattern we use."
The Verification Principle
Tests are the ultimate form of verification for Claude's work. When Claude writes code, tests provide objective, automated feedback about correctness.
Why Tests Matter for AI Collaboration
Traditional development: You write code, then write tests to verify it.
AI-assisted development: The AI writes code, tests verify it worked correctly.
This inverts the traditional relationship. Tests become:
- Specification: Tests define "correct" behavior
- Validation: Tests confirm Claude's changes work
- Regression protection: Tests catch when future changes break existing functionality
- Living documentation: Tests show how code is meant to be used
Always Provide Success Criteria
When asking Claude to make changes, include how you'll verify success:
Vague: "Improve error handling in the notification service"
Specific: "Improve error handling in the notification service. When done, all existing tests should pass and coverage should be above 85%."
The specific version gives Claude:
- A clear goal (85% coverage)
- A verification method (run tests)
- An objective pass/fail signal
The Test-Fix Loop
The most powerful pattern in AI-assisted development:
- Claude makes changes (new feature, refactor, bug fix)
- Claude runs tests to verify the changes
- If tests fail, Claude reads the error output
- Claude fixes the issue based on test feedback
- Repeat until tests pass
This loop requires no human intervention. Claude gets immediate, unambiguous feedback and can iterate until the code is correct.
Key Takeaway
Tests transform AI assistance from "generate and hope" to "generate and verify." Always run tests after Claude makes changes, and let test failures guide Claude's fixes.
Best Practices for Test and Documentation Generation
Generate Tests and Documentation for an Untested Module
beginnerTask: Generate comprehensive tests and documentation for an untested module in your codebase.
Steps:
-
Identify an untested module in your project (e.g.,
src/services/EmailService.ts,lib/utils/validators.py) -
Analyze coverage gaps:
Find functions in src/services/EmailService.ts that are missing tests -
Generate basic test scaffolding:
Add tests for EmailService following the patterns in existing test files -
Add edge case tests:
Add test cases for edge conditions and error handling in EmailService -
Run and verify tests:
Run the EmailService tests and fix any failures -
Check coverage:
Run coverage analysis for EmailService and report the results -
Improve coverage (if below 80%):
Add tests to achieve 80% line coverage for EmailService -
Generate documentation:
Add JSDoc comments to all public functions in EmailService with examples -
Review and refine:
- Read the generated tests—do they verify meaningful behavior?
- Read the documentation—is it clear and helpful?
- Ask Claude to improve any unclear sections
Success criteria:
- All tests pass
- Coverage is at least 80% for the module
- All public functions have clear documentation with examples
- Generated tests cover happy paths, edge cases, and error conditions
Bonus challenge: Create an integration test that exercises the module with real dependencies (database, external API, file system) using appropriate mocks.
Summary
Test and documentation generation with Claude Code transforms tedious maintenance tasks into automated workflows. By analyzing existing patterns, Claude can generate tests that match your conventions, identify edge cases you might miss, and create documentation that explains intent alongside implementation.
The verification principle is central to effective AI collaboration: tests provide objective feedback that guides Claude's iterations. When you ask Claude to make changes, always include how you'll verify success (run tests, check coverage, validate output). This creates a tight feedback loop where Claude can autonomously verify and fix its own work.
Next Steps
In the next lesson, you'll learn about Prompt Engineering for Agentic Coding — how to calibrate prompt specificity based on task type, provide rich context using files and URLs, use the Claude interview technique for complex features, and avoid common prompting mistakes that waste correction cycles.