--- name: java-development description: Java development patterns with Spring Boot, JUnit 5, and modern Java practices. Use when writing Java code. --- # Java Development Skill ## Project Structure (Spring Boot) ``` project/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/mycompany/myapp/ │ │ │ ├── MyApplication.java │ │ │ ├── domain/ │ │ │ │ ├── User.java │ │ │ │ └── Order.java │ │ │ ├── repository/ │ │ │ │ └── UserRepository.java │ │ │ ├── service/ │ │ │ │ └── UserService.java │ │ │ ├── controller/ │ │ │ │ └── UserController.java │ │ │ └── config/ │ │ │ └── SecurityConfig.java │ │ └── resources/ │ │ ├── application.yml │ │ └── application-test.yml │ └── test/ │ └── java/ │ └── com/mycompany/myapp/ │ ├── service/ │ │ └── UserServiceTest.java │ └── controller/ │ └── UserControllerTest.java ├── pom.xml └── README.md ``` ## Testing with JUnit 5 ### Unit Tests with Mockito ```java package com.mycompany.myapp.service; import com.mycompany.myapp.domain.User; import com.mycompany.myapp.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Nested @DisplayName("createUser") class CreateUser { @Test @DisplayName("should create user with valid data") void shouldCreateUserWithValidData() { // Given var request = UserFixtures.createUserRequest(); var savedUser = UserFixtures.user(); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When var result = userService.createUser(request); // Then assertThat(result.getId()).isNotNull(); assertThat(result.getEmail()).isEqualTo(request.getEmail()); verify(userRepository).save(any(User.class)); } @Test @DisplayName("should throw exception for duplicate email") void shouldThrowExceptionForDuplicateEmail() { // Given var request = UserFixtures.createUserRequest(); when(userRepository.existsByEmail(request.getEmail())).thenReturn(true); // When/Then assertThatThrownBy(() -> userService.createUser(request)) .isInstanceOf(DuplicateEmailException.class) .hasMessageContaining(request.getEmail()); } } @Nested @DisplayName("getUserById") class GetUserById { @Test @DisplayName("should return user when found") void shouldReturnUserWhenFound() { // Given var user = UserFixtures.user(); when(userRepository.findById(user.getId())).thenReturn(Optional.of(user)); // When var result = userService.getUserById(user.getId()); // Then assertThat(result).isPresent(); assertThat(result.get().getId()).isEqualTo(user.getId()); } @Test @DisplayName("should return empty when user not found") void shouldReturnEmptyWhenNotFound() { // Given when(userRepository.findById(any())).thenReturn(Optional.empty()); // When var result = userService.getUserById("unknown-id"); // Then assertThat(result).isEmpty(); } } } ``` ### Test Fixtures ```java package com.mycompany.myapp.fixtures; import com.mycompany.myapp.domain.User; import com.mycompany.myapp.dto.CreateUserRequest; import java.time.Instant; import java.util.UUID; public final class UserFixtures { private UserFixtures() {} public static User user() { return user(builder -> {}); } public static User user(UserCustomizer customizer) { var builder = User.builder() .id(UUID.randomUUID().toString()) .email("test@example.com") .name("Test User") .role(Role.USER) .createdAt(Instant.now()) .updatedAt(Instant.now()); customizer.customize(builder); return builder.build(); } public static CreateUserRequest createUserRequest() { return createUserRequest(builder -> {}); } public static CreateUserRequest createUserRequest(CreateUserRequestCustomizer customizer) { var builder = CreateUserRequest.builder() .email("newuser@example.com") .name("New User") .password("SecurePass123!"); customizer.customize(builder); return builder.build(); } @FunctionalInterface public interface UserCustomizer { void customize(User.UserBuilder builder); } @FunctionalInterface public interface CreateUserRequestCustomizer { void customize(CreateUserRequest.CreateUserRequestBuilder builder); } } // Usage in tests var adminUser = UserFixtures.user(builder -> builder.role(Role.ADMIN)); var customRequest = UserFixtures.createUserRequest(builder -> builder.email("custom@example.com")); ``` ### Parameterized Tests ```java package com.mycompany.myapp.validation; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.*; class EmailValidatorTest { private final EmailValidator validator = new EmailValidator(); @ParameterizedTest @DisplayName("should accept valid emails") @ValueSource(strings = { "user@example.com", "user.name@example.com", "user+tag@example.co.uk" }) void shouldAcceptValidEmails(String email) { assertThat(validator.isValid(email)).isTrue(); } @ParameterizedTest @DisplayName("should reject invalid emails") @ValueSource(strings = { "invalid", "missing@domain", "@nodomain.com", "spaces in@email.com" }) void shouldRejectInvalidEmails(String email) { assertThat(validator.isValid(email)).isFalse(); } @ParameterizedTest @DisplayName("should reject null and empty emails") @NullAndEmptySource void shouldRejectNullAndEmpty(String email) { assertThat(validator.isValid(email)).isFalse(); } @ParameterizedTest @DisplayName("should validate password strength") @CsvSource({ "password,false", "Password1,false", "Password1!,true", "P@ssw0rd,true" }) void shouldValidatePasswordStrength(String password, boolean expected) { assertThat(validator.isStrongPassword(password)).isEqualTo(expected); } } ``` ### Integration Tests with Spring ```java package com.mycompany.myapp.controller; import com.mycompany.myapp.fixtures.UserFixtures; import com.mycompany.myapp.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; @Autowired private UserRepository userRepository; @BeforeEach void setUp() { userRepository.deleteAll(); } @Test @DisplayName("POST /api/users creates user and returns 201") void createUserReturnsCreated() throws Exception { var request = """ { "email": "new@example.com", "name": "New User", "password": "SecurePass123!" } """; mockMvc.perform(post("/api/users") .contentType(MediaType.APPLICATION_JSON) .content(request)) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").isNotEmpty()) .andExpect(jsonPath("$.email").value("new@example.com")) .andExpect(jsonPath("$.name").value("New User")); } @Test @DisplayName("GET /api/users/{id} returns user when found") void getUserReturnsUserWhenFound() throws Exception { var user = userRepository.save(UserFixtures.user()); mockMvc.perform(get("/api/users/{id}", user.getId())) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(user.getId())) .andExpect(jsonPath("$.email").value(user.getEmail())); } @Test @DisplayName("GET /api/users/{id} returns 404 when not found") void getUserReturns404WhenNotFound() throws Exception { mockMvc.perform(get("/api/users/{id}", "unknown-id")) .andExpect(status().isNotFound()); } } ``` ## Domain Models with Records ```java package com.mycompany.myapp.domain; import jakarta.validation.constraints.*; import java.time.Instant; // Immutable domain model using records (Java 17+) public record User( String id, @NotBlank @Email String email, @NotBlank @Size(max = 100) String name, Role role, Instant createdAt, Instant updatedAt ) { // Compact constructor for validation public User { if (email != null) { email = email.toLowerCase().trim(); } } // Static factory methods public static User create(String email, String name) { var now = Instant.now(); return new User( UUID.randomUUID().toString(), email, name, Role.USER, now, now ); } // Wither methods for immutable updates public User withName(String newName) { return new User(id, email, newName, role, createdAt, Instant.now()); } public User withRole(Role newRole) { return new User(id, email, name, newRole, createdAt, Instant.now()); } } // For mutable entities (JPA) @Entity @Table(name = "users") @Getter @Builder @NoArgsConstructor @AllArgsConstructor public class UserEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @Column(unique = true, nullable = false) private String email; @Column(nullable = false) private String name; @Enumerated(EnumType.STRING) private Role role; @Column(name = "created_at") private Instant createdAt; @Column(name = "updated_at") private Instant updatedAt; @PrePersist void onCreate() { createdAt = Instant.now(); updatedAt = createdAt; } @PreUpdate void onUpdate() { updatedAt = Instant.now(); } } ``` ## Service Layer ```java package com.mycompany.myapp.service; import com.mycompany.myapp.domain.User; import com.mycompany.myapp.dto.CreateUserRequest; import com.mycompany.myapp.exception.DuplicateEmailException; import com.mycompany.myapp.exception.UserNotFoundException; import com.mycompany.myapp.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; @Transactional public User createUser(CreateUserRequest request) { if (userRepository.existsByEmail(request.getEmail())) { throw new DuplicateEmailException(request.getEmail()); } var user = User.create( request.getEmail(), request.getName() ); return userRepository.save(user); } public Optional getUserById(String id) { return userRepository.findById(id); } public User getUserByIdOrThrow(String id) { return userRepository.findById(id) .orElseThrow(() -> new UserNotFoundException(id)); } @Transactional public User updateUser(String id, UpdateUserRequest request) { var user = getUserByIdOrThrow(id); var updatedUser = user .withName(request.getName()) .withRole(request.getRole()); return userRepository.save(updatedUser); } @Transactional public void deleteUser(String id) { if (!userRepository.existsById(id)) { throw new UserNotFoundException(id); } userRepository.deleteById(id); } } ``` ## REST Controllers ```java package com.mycompany.myapp.controller; import com.mycompany.myapp.dto.*; import com.mycompany.myapp.service.UserService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/users") @RequiredArgsConstructor public class UserController { private final UserService userService; private final UserMapper userMapper; @GetMapping public List listUsers() { return userService.getAllUsers().stream() .map(userMapper::toResponse) .toList(); } @GetMapping("/{id}") public ResponseEntity getUser(@PathVariable String id) { return userService.getUserById(id) .map(userMapper::toResponse) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @PostMapping @ResponseStatus(HttpStatus.CREATED) public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) { var user = userService.createUser(request); return userMapper.toResponse(user); } @PutMapping("/{id}") public UserResponse updateUser( @PathVariable String id, @Valid @RequestBody UpdateUserRequest request) { var user = userService.updateUser(id, request); return userMapper.toResponse(user); } @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteUser(@PathVariable String id) { userService.deleteUser(id); } } ``` ## Exception Handling ```java package com.mycompany.myapp.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.net.URI; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(UserNotFoundException.class) public ProblemDetail handleUserNotFound(UserNotFoundException ex) { var problem = ProblemDetail.forStatusAndDetail( HttpStatus.NOT_FOUND, ex.getMessage() ); problem.setTitle("User Not Found"); problem.setType(URI.create("https://api.myapp.com/errors/user-not-found")); return problem; } @ExceptionHandler(DuplicateEmailException.class) public ProblemDetail handleDuplicateEmail(DuplicateEmailException ex) { var problem = ProblemDetail.forStatusAndDetail( HttpStatus.CONFLICT, ex.getMessage() ); problem.setTitle("Duplicate Email"); problem.setType(URI.create("https://api.myapp.com/errors/duplicate-email")); return problem; } @ExceptionHandler(MethodArgumentNotValidException.class) public ProblemDetail handleValidation(MethodArgumentNotValidException ex) { var problem = ProblemDetail.forStatusAndDetail( HttpStatus.BAD_REQUEST, "Validation failed" ); problem.setTitle("Validation Error"); var errors = ex.getBindingResult().getFieldErrors().stream() .map(e -> new ValidationError(e.getField(), e.getDefaultMessage())) .toList(); problem.setProperty("errors", errors); return problem; } record ValidationError(String field, String message) {} } ``` ## Commands ```bash # Maven mvn clean install # Build and test mvn test # Run tests only mvn verify # Run integration tests mvn spring-boot:run # Run application mvn jacoco:report # Generate coverage report # Gradle ./gradlew build # Build and test ./gradlew test # Run tests only ./gradlew integrationTest # Run integration tests ./gradlew bootRun # Run application ./gradlew jacocoTestReport # Generate coverage report # Common flags -DskipTests # Skip tests -Dspring.profiles.active=test # Set profile ``` ## Dependencies (pom.xml) ```xml org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-data-jpa org.projectlombok lombok provided org.springframework.boot spring-boot-starter-test test org.assertj assertj-core test ```