Índice de temario
Día | Tema a tratar |
---|---|
1 | Definiendo objetivos |
2 | Proyecto inicial |
3 | Endpoint básico |
4 | Entidades y persistencia |
5 | Transaccionalidad |
6 | Migraciones |
7 | Manejo de excepciones |
Día 7. Manejo de excepciones en el controlador
Ahora necesitamos poder manejar excepciones de forma controlada. Cualquier cosa que vaya mal en el flujo se puede capturar en en controlador y mostrar un mensaje coherente.
Para nuestro ejemplo, vamos a agregarle un password al usuario y vamos a decidir fallar si dicho password tiene menos de 6 caracteres.
Usaremos Liquibase para la migración, con lo que creamos un fichero nuevo y le ponemos la modificación para aceptar passwords.
El changeset acepta ponerle un identificador. Yo uso el formato YYYYMMDDHHMM para generar uno, y así me hago una idea de cuándo se creó si luego se solapa con otras migraciones al mergear con master.
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd
http://www.liquibase.org/xml/ns/pro
http://www.liquibase.org/xml/ns/pro/liquibase-pro-4.1.xsd">
<changeSet id="202201061529" author="Fernando Aparicio">
<addColumn tableName="users">
<column name="password" type="varchar(250)">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>
No nos vamos a entretener explicando cómo montar los objetos con sus tests, ni la capa de persistencia para soportarlo, ya que lo hemos visto con anterioridad en la serie de posts de “php a kotlin” y esta misma serie dedicada a Spring con kotlin. Simplemente mostraré una posible implementación del value object que verifica el password.
En este caso es el value object de UserPassword.
package com.example.users.user.domain.valueobjects
import com.example.users.user.domain.exceptions.InvalidPasswordException
class UserPassword private constructor(val value: String){
init {
if (MINIMUM_PASSWORD_LENGTH > value.length) throw InvalidPasswordException.passwordTooShort()
}
companion object {
private const val MINIMUM_PASSWORD_LENGTH = 6
@JvmStatic
fun build(password: String): UserPassword = UserPassword(password)
}
}
Ahora si vamos al test del controller SignUpUser que hicimos al principio sin tocar nada más que la migración, fallará por un motivo evidente. La creación del usuario falla porque falta un campo en nuestra entidad. Pero vamos a entrar más en detalle de porqué ha fallado el test.
El test falla porque consultamos si existe dicho usuario creado en la base de datos. Sin esta comprobación, tendríamos un test en verde pero el comportamiento sería incorrecto, ya que el caso de uso en la capa de aplicación hace rollback de la transacción, pero el controller sigue dando un 201 (CREATED) como si todo fuera correcto…
@Test
@Transactional
fun `Should create a User` () {
// Aquí funciona
mockMvc.perform(
post("/user/signup").param("username", "Foo"))
.andExpect(status().isCreated)
// Aquí se descubre que no se ha creado el usuario realmente
val user = userRepository.findByUsername(UserName.build("Foo"))
assertThat(user).isNotNull
if (null != user) {
assertThat(user.userName.value).isEqualTo("Foo")
}
}
O sea que el controller necesita un poco más de trabajo.
En este caso, decidimos que un error de este tipo requiere un error 400 (Error de petición incorrecta).
La gestión de excepciones
En Spring hay varias estrategias. Vamos a usar la más sencilla, que es hacer un try-catch y gestionar los errores controlados.
El fichero SignUpUser lo pondremos de esta manera:
package com.example.users.user.ui.signupuser
import com.example.users.user.application.commands.SignUpUserCommand
import com.example.users.user.application.commands.SignUpUserHandler
import com.example.users.user.domain.exceptions.InvalidPasswordException
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import kotlin.reflect.jvm.internal.impl.load.kotlin.JvmType
@RestController
class SignUpUser @Autowired constructor(private val signUpUser: SignUpUserHandler){
@PostMapping("/user/signup")
fun execute(
@RequestParam("username") username: String,
@RequestParam("password") password: String
): ResponseEntity<JvmType.Object>
{
try {
signUpUser.execute(SignUpUserCommand(username, password))
} catch (exception: InvalidPasswordException) {
return ResponseEntity(HttpStatus.BAD_REQUEST)
} catch (error: Exception) {
return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR)
}
return ResponseEntity(HttpStatus.CREATED)
}
}
Y nos aseguramos que los dos tests de integración pasan en verde.
Links y recursos
Exception handler strategies
https://www.baeldung.com/exception-handling-for-rest-with-spring
Best practices
https://www.baeldung.com/rest-api-error-handling-best-practices
Twitter
Facebook
Reddit
LinkedIn
Email