Let’s walk through a complete Test-Driven Development cycle using Claude AI assistance, from initial requirements to fully tested code. We’ll build a user registration system that demonstrates how AI can accelerate and improve your TDD workflow.
The Requirements
Our product manager provides these requirements for a user registration feature:
“We need a user registration system that accepts email and password. Email must be valid format and unique in our system. Password must be at least 8 characters with one uppercase, one lowercase, and one number. We should hash passwords before storing and return appropriate error messages for validation failures.”
Step 1: Translating Requirements to Test Cases with Claude
First, I asked Claude: “Based on these requirements, what test cases should I write for a UserRegistrationService?”
Claude suggested these test scenarios:
Valid registration with proper email and password
Invalid email formats (missing @, invalid domain, empty)
Duplicate email registration attempts
Password validation (too short, missing uppercase/lowercase/number)
Password hashing verification
Proper error message returns
Step 2: Setting Up the Test Structure
Let’s start with our test class structure:
package com.example.registration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; public class UserRegistrationServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordHasher passwordHasher; private UserRegistrationService registrationService; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); registrationService = new UserRegistrationService(userRepository, passwordHasher); } }
Step 3: Writing Failing Tests (Red Phase)
Claude helped me write comprehensive failing tests:
@Test void shouldRegisterUserWithValidEmailAndPassword() { // Arrange String email = "john.doe@example.com"; String password = "SecurePass123"; String hashedPassword = "hashed_password_here"; when(userRepository.existsByEmail(email)).thenReturn(false); when(passwordHasher.hash(password)).thenReturn(hashedPassword); // Act RegistrationResult result = registrationService.registerUser(email, password); // Assert assertTrue(result.isSuccess()); assertNull(result.getErrorMessage()); verify(userRepository).save(any(User.class)); } @Test void shouldRejectInvalidEmailFormat() { // Arrange String invalidEmail = "not-an-email"; String password = "ValidPass123"; // Act RegistrationResult result = registrationService.registerUser(invalidEmail, password); // Assert assertFalse(result.isSuccess()); assertEquals("Invalid email format", result.getErrorMessage()); verify(userRepository, never()).save(any(User.class)); } @Test void shouldRejectDuplicateEmail() { // Arrange String existingEmail = "existing@example.com"; String password = "ValidPass123"; when(userRepository.existsByEmail(existingEmail)).thenReturn(true); // Act RegistrationResult result = registrationService.registerUser(existingEmail, password); // Assert assertFalse(result.isSuccess()); assertEquals("Email already registered", result.getErrorMessage()); verify(userRepository, never()).save(any(User.class)); } @Test void shouldRejectWeakPassword() { // Arrange String email = "test@example.com"; String weakPassword = "weak"; when(userRepository.existsByEmail(email)).thenReturn(false); // Act RegistrationResult result = registrationService.registerUser(email, weakPassword); // Assert assertFalse(result.isSuccess()); assertEquals("Password must be at least 8 characters with uppercase, lowercase, and number", result.getErrorMessage()); verify(userRepository, never()).save(any(User.class)); } @Test void shouldHashPasswordBeforeStoring() { // Arrange String email = "secure@example.com"; String password = "MySecurePass123"; String hashedPassword = "hashed_version"; when(userRepository.existsByEmail(email)).thenReturn(false); when(passwordHasher.hash(password)).thenReturn(hashedPassword); // Act registrationService.registerUser(email, password); // Assert ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); verify(userRepository).save(userCaptor.capture()); User savedUser = userCaptor.getValue(); assertEquals(email, savedUser.getEmail()); assertEquals(hashedPassword, savedUser.getPasswordHash()); assertNotEquals(password, savedUser.getPasswordHash()); // Ensure it's hashed }
Step 4: Creating Supporting Classes
Claude helped me identify the needed supporting classes:
// RegistrationResult.java public class RegistrationResult { private final boolean success; private final String errorMessage; private RegistrationResult(boolean success, String errorMessage) { this.success = success; this.errorMessage = errorMessage; } public static RegistrationResult success() { return new RegistrationResult(true, null); } public static RegistrationResult failure(String errorMessage) { return new RegistrationResult(false, errorMessage); } public boolean isSuccess() { return success; } public String getErrorMessage() { return errorMessage; } } // User.java public class User { private String email; private String passwordHash; public User(String email, String passwordHash) { this.email = email; this.passwordHash = passwordHash; } // Getters public String getEmail() { return email; } public String getPasswordHash() { return passwordHash; } } // UserRepository.java (interface) public interface UserRepository { boolean existsByEmail(String email); void save(User user); } // PasswordHasher.java (interface) public interface PasswordHasher { String hash(String password); }
Step 5: Implementing the Service (Green Phase)
Now let’s make the tests pass with the minimal implementation:
package com.example.registration; import java.util.regex.Pattern; public class UserRegistrationService { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"); private static final Pattern PASSWORD_PATTERN = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"); private final UserRepository userRepository; private final PasswordHasher passwordHasher; public UserRegistrationService(UserRepository userRepository, PasswordHasher passwordHasher) { this.userRepository = userRepository; this.passwordHasher = passwordHasher; } public RegistrationResult registerUser(String email, String password) { // Validate email format if (!isValidEmail(email)) { return RegistrationResult.failure("Invalid email format"); } // Check for duplicate email if (userRepository.existsByEmail(email)) { return RegistrationResult.failure("Email already registered"); } // Validate password strength if (!isValidPassword(password)) { return RegistrationResult.failure( "Password must be at least 8 characters with uppercase, lowercase, and number"); } // Hash password and save user String hashedPassword = passwordHasher.hash(password); User user = new User(email, hashedPassword); userRepository.save(user); return RegistrationResult.success(); } private boolean isValidEmail(String email) { return email != null && EMAIL_PATTERN.matcher(email).matches(); } private boolean isValidPassword(String password) { return password != null && PASSWORD_PATTERN.matcher(password).matches(); } }
Step 6: Refactoring with Claude’s Help
Claude suggested several improvements for the refactor phase:
Extract validation logic into separate validator classes
Add more specific password validation messages
Create custom exceptions for different error types
Here’s the refactored version:
// EmailValidator.java public class EmailValidator { private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$"); public ValidationResult validate(String email) { if (email == null || email.trim().isEmpty()) { return ValidationResult.invalid("Email is required"); } if (!EMAIL_PATTERN.matcher(email).matches()) { return ValidationResult.invalid("Invalid email format"); } return ValidationResult.valid(); } } // PasswordValidator.java public class PasswordValidator { public ValidationResult validate(String password) { if (password == null || password.length() < 8) { return ValidationResult.invalid("Password must be at least 8 characters"); } if (!password.matches(".*[a-z].*")) { return ValidationResult.invalid("Password must contain lowercase letter"); } if (!password.matches(".*[A-Z].*")) { return ValidationResult.invalid("Password must contain uppercase letter"); } if (!password.matches(".*\\d.*")) { return ValidationResult.invalid("Password must contain a number"); } return ValidationResult.valid(); } } // Updated UserRegistrationService public class UserRegistrationService { private final UserRepository userRepository; private final PasswordHasher passwordHasher; private final EmailValidator emailValidator; private final PasswordValidator passwordValidator; public UserRegistrationService(UserRepository userRepository, PasswordHasher passwordHasher, EmailValidator emailValidator, PasswordValidator passwordValidator) { this.userRepository = userRepository; this.passwordHasher = passwordHasher; this.emailValidator = emailValidator; this.passwordValidator = passwordValidator; } public RegistrationResult registerUser(String email, String password) { ValidationResult emailValidation = emailValidator.validate(email); if (!emailValidation.isValid()) { return RegistrationResult.failure(emailValidation.getErrorMessage()); } if (userRepository.existsByEmail(email)) { return RegistrationResult.failure("Email already registered"); } ValidationResult passwordValidation = passwordValidator.validate(password); if (!passwordValidation.isValid()) { return RegistrationResult.failure(passwordValidation.getErrorMessage()); } String hashedPassword = passwordHasher.hash(password); User user = new User(email, hashedPassword); userRepository.save(user); return RegistrationResult.success(); } }
Step 7: Additional Test Cases with Claude
Claude suggested edge cases I hadn’t considered:
@Test void shouldRejectNullEmail() { RegistrationResult result = registrationService.registerUser(null, "ValidPass123"); assertFalse(result.isSuccess()); assertEquals("Email is required", result.getErrorMessage()); } @Test void shouldRejectEmptyPassword() { RegistrationResult result = registrationService.registerUser("test@example.com", ""); assertFalse(result.isSuccess()); assertEquals("Password must be at least 8 characters", result.getErrorMessage()); } @Test void shouldHandleRepositoryExceptions() { String email = "test@example.com"; String password = "ValidPass123"; when(userRepository.existsByEmail(email)).thenReturn(false); when(passwordHasher.hash(password)).thenReturn("hashed"); doThrow(new RuntimeException("Database error")).when(userRepository).save(any(User.class)); assertThrows(RuntimeException.class, () -> { registrationService.registerUser(email, password); }); }
Key Benefits of AI-Assisted TDD
Through this real-world example, Claude AI provided several advantages:
Comprehensive Test Coverage: Claude identified edge cases like null inputs and database exceptions that I might have missed initially.
Better Code Structure: The AI suggested extracting validators into separate classes, improving maintainability and single responsibility principle adherence.
Realistic Mock Scenarios: Claude helped create meaningful mock interactions that truly test the service behavior.
Progressive Refinement: As requirements evolved, Claude helped adapt tests and suggest improvements without breaking existing functionality.
Conclusion
This practical example demonstrates how Claude AI transforms TDD from a sometimes tedious process into an efficient, comprehensive development approach. By leveraging AI assistance for test generation, edge case identification, and code structure suggestions, developers can focus on business logic while ensuring robust test coverage.
The key is treating Claude as a knowledgeable pair programming partner who helps you think through scenarios, suggests improvements, and catches potential issues early in the development cycle. The result is higher-quality, well-tested code delivered more efficiently than traditional manual TDD approaches.
Top comments (0)