3 minute read

Índice de temario

Día Tema a tratar
1 Definiendo objetivos
2 Proyecto inicial y value objects
3 Tratando con excepciones
4 Validando con Enum y parametrized test
5 Ampliando uso del Enum
6 Entidades

Día 4. Validando un value object con Enum y Parameterized test

Lo interesante de los Enum en Kotlin es que es muy fácil agregarle contexto.

Aunque los Enum en Kotlin tienen muchas posibilidades, usaremos lo justo y necesario para nuestras necesidades.

Para verlo, usaremos el Value Object MowerOrientation.

Como siempre, montamos un escenario de test para el empezar a trabajar.

import mower.mower.exception.InvalidOrientationException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import kotlin.test.Test

private const val ORIENTATION = "N"
private const val INVALID_ORIENTATION = "H"

internal class MowerOrientationTest {
    @Test
    fun `Should be build`() {
        val orientation = MowerOrientation.build(ORIENTATION)

        assertInstanceOf(MowerOrientation::class.java, orientation)
        assertThat(orientation.value).isEqualTo(ORIENTATION)
    }

    @ParameterizedTest(name = "{index} => orientation = ''{0}''")
    @ValueSource(strings = [ "N", "S", "E", "W" ])
    fun `Should be build with valid orientations`(value: String) {
        val orientation = MowerOrientation.build(value)

        assertThat(orientation.value).isEqualTo(value)
    }

    @Test
    fun `Should throw exception for invalid Orientation`() {

        assertThrows(InvalidOrientationException::class.java) {
            MowerOrientation.build(INVALID_ORIENTATION)
        }
    }
}

Queremos validar que el valor que le pasamos al value object cumple con los cuatro direcciones posibles. También queremos un error personalizado y que el código siga siendo elegante (sin try-catch)

Fijaos cómo se configura un test con varios valores posibles. El ParameterizedTest en JUnit es el equivalente al DataProvider de PHPUnit. Y podemos agregarle contexto al lanzarlos con una plantilla como la que hay descrita en el test.

...
    @ParameterizedTest(name = "{index} => orientation = ''{0}''")
...

Contexto en tests

El objeto requerido lo podemos articular de la siguiente manera.

import mower.mower.exception.InvalidOrientationException

@JvmInline
value class MowerOrientation private constructor(val value: String) {
    private enum class Compass(val value: String){
        NORTH("N"),
        EAST("E"),
        SOUTH("S"),
        WEST("W")
    }

    init {
        if(null === enumValues<Compass>().find { it.value == value}){
            throw InvalidOrientationException.withValues(value, Orientations.values().map { it.value }.toString())
        }
    }

    @JvmStatic
    companion object {
        fun build(value: String): MowerOrientation
        {
            return MowerOrientation(value)
        }
    }
}

Lo más interesante es que podemos crear una constante, que nos sirve de contexto, y un valor asociado, que es el que el sistema usará para inicializar el valor del value object. Hay más formas de alimentar los parámetros en la documentación que os pongo más abajo.

Ojo dilema…

Kotlin es más partidario de lanzar excepciones que de usar el valor null como respuesta. Podemos hacer varios cambios para cumplir con sus requisitos. Nos ahorraríamos el coste de procesar con el find y el map en caso de responder con error.

Aunque prefiero la primera implementación, entiendo que si nosotros no controlamos el null, podemos tener un problema oculto en nuestro código, cosa que con una excepción no tratada no sucede.

    ...
    private enum class Compass{ N, E, S, W }

    init {
        try {
            Compass.valueOf(value)
        } catch (exception: IllegalArgumentException) {
            throw InvalidOrientationException.withValues(value, Compass.values().contentToString())
        }
    }
    ...

Si luego queremos usar el enum para recuperar información, de la primera forma debemos usar el find y tratar el null, cosa no óptima. Para mí es el eterno dilema… y no hay una respuesta firme. Depende… como en todo…

Lo más terrible que he visto es una escalada de try-catch entre objetos que llaman a otros, y eso suele acabar mal… difícil de leer y gestionar.

En este caso creo que es más acertado seguir el camino de Kotlin y lo dejaremos con el manejo de excepciones. Ya que más adelante deberemos usar los valores del Enum para otras cosas y estaríamos haciendo find cada vez, con lo que nos cargamos la utilidad de la funcionalidad.


Enum class

https://kotlinlang.org/docs/enum-classes.html

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/enum-values.html

Parameterized test sources

https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources

https://www.petrikainulainen.net/programming/testing/junit-5-tutorial-writing-parameterized-tests/


comments powered by Disqus