3 minute read

Í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.


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


comments powered by Disqus