Spring Boot y los Building Blocks
Con Spring Boot se vuelve bastante simple implementar los patrones tácticos requeridos en este proyecto.
1. Entity
Las Entities (Entidades) son representaciones de objetos del dominio que poseen una identidad única y cuyo estado puede cambiar con el tiempo. En Spring Boot, las entidades a menudo se anotan con @Entity.
Ejemplo:
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class User {
@Id
private Long id;
private String name;
private String email;
// Constructores, getters y setters
public User() {}
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getters y Setters
}
2. Repository
El Repository (Repositorio) actúa como un intermediario entre la aplicación y la capa de persistencia de datos. En Spring Boot, esto se hace frecuentemente usando Spring Data JPA, que simplifica la implementación de repositorios.
Ejemplo:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> {
// Método para encontrar un usuario por su e-mail
User findByEmail(String email);
// Método para encontrar todos los usuarios con una edad mayor a un valor específico
@Query("SELECT u FROM User u WHERE u.age > :age")
List<User> findAllOlderThan(@Param("age") int age);
}
3. Service
El Service (Servicio) es responsable de implementar la lógica de negocio de la aplicación. Orquesta las interacciones entre las entidades y los repositorios.
Ejemplo:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
return userRepository.save(user);
}
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
public List<User> getAllUsers() {
return userRepository.findAll();
}
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
4. Controller
El Controller (Controlador) maneja las solicitudes del usuario y coordina la interacción entre la interfaz de usuario y los servicios. En Spring Boot, los controladores se anotan con @RestController.
Ejemplo:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(createdUser);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
return user != null ? ResponseEntity.ok(user) : ResponseEntity.notFound().build();
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
5. TDD
Probar utilizando @SpringBootTest con MockMvc es una práctica eficaz que se alinea con los principios del Desarrollo Guiado por Pruebas (TDD). Este enfoque permite validar las interacciones del controlador con el servicio de forma aislada, garantizando que la lógica de manipulación de usuarios funcione correctamente, sin depender de componentes externos como una base de datos real. Al crear pruebas antes de la implementación, TDD ayuda a definir claramente los requisitos y comportamientos esperados del sistema, promoviendo la escritura de código más robusto. Esta combinación resulta en un ciclo de desarrollo más eficiente, donde la funcionalidad del sistema puede ser validada y refinada constantemente, asegurando que permanezca confiable y adaptable a medida que evoluciona.
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mockMvc;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper();
}
@Test
void testCreateUser() throws Exception {
User user = new User(1L, "John Doe", "john@example.com");
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void testGetUser() throws Exception {
User user = new User(1L, "John Doe", "john@example.com");
mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Doe"))
.andExpect(jsonPath("$.email").value("john@example.com"));
}
@Test
void testGetUserNotFound() throws Exception {
mockMvc.perform(get("/users/999"))
.andExpect(status().isNotFound());
}
@Test
void testGetAllUsers() throws Exception {
mockMvc.perform(get("/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("John Doe"))
.andExpect(jsonPath("$[1].name").value("Jane Doe"));
}
@Test
void testDeleteUser() throws Exception {
mockMvc.perform(delete("/users/1"))
.andExpect(status().isNoContent());
}
}