ALBERT ORTELLS // TECH TESTS

CASO · ONEBOX

Cómo resolví la prueba técnica de OneBox

Java · Spring Boot · En memoria · 2023 · RESUELTA

Todo empezó con un único documento: la prueba técnica de OneBox (Ticket Distribution System), fechada en 2023. Un servicio pequeño de e-commerce —carritos, productos, expiración por inactividad— con una advertencia clara sobre qué se iba a valorar: que cumpliera los requisitos, una cobertura de tests mínima, y que fuera fácil de probar, revisar y desplegar. Nada de sistemas grandes ni de funcionalidad de sobra.

El reto

El enunciado pedía cinco comportamientos muy concretos:

El único requisito técnico impuesto era usar Java. Todo lo demás —stack, arquitectura, alcance de cada pieza— quedaba en mi mano, y ahí es donde entran las decisiones que de verdad definen el resultado.

Construir de fuera hacia dentro

A diferencia de otras pruebas donde el contrato de entrada y salida viene totalmente fijado por el enunciado y tiene sentido modelar primero el dominio, aquí el enunciado describía comportamientos ("crear un carrito", "añadir productos", "eliminarlo") sin imponer ninguna forma de API concreta. Diseñar el dominio antes de saber exactamente qué necesitaba exponer cada operación habría significado adivinar una forma para Cart y Product antes de tener motivo real para esa forma.

Así que invertí el orden: por cada operación, empecé por el punto de entrada —el controller REST, o el scheduler en el caso de la expiración— devolviendo una respuesta mínima construida ahí mismo, sin capa de servicio todavía. Eso me daba de inmediato el contrato verificado por un test: verbo HTTP, ruta, código de estado, forma del cuerpo. Solo una vez fijado ese contrato, empujaba la lógica hacia dentro: primero a un CartService que seguía sin tocar ninguna persistencia, y por último a un CartRepository en memoria (un ConcurrentHashMap, sin base de datos real, porque el enunciado no pedía persistencia y añadirla habría sido peso de más).

El primer intento de este patrón se me fue de las manos: en vez de tocar solo el controller de un flujo, construí también el servicio y el repositorio de golpe. Sirvió para fijar la regla que seguí en todo lo demás: cada capa se entrega, se verifica y se confirma antes de tocar la siguiente, nunca varias a la vez.

La decisión que más pesé: cómo comunicar "el carrito no existe"

Con las cuatro operaciones y la expiración ya en marcha, el camino feliz funcionaba y estaba testeado, pero consultar, modificar o borrar un carrito inexistente no tenía un tratamiento explícito: en unos casos se devolvía null, en otros se propagaba un error interno sin control. Necesitaba una única forma consistente de comunicar ese caso en las tres operaciones que dependen de que el carrito exista.

La alternativa más directa habría sido crear también un DTO de error propio junto a la excepción, pero eso repite exactamente lo que había evitado desde el principio: introducir estructuras nuevas sin necesidad real. Preferí una CartNotFoundException de dominio, traducida a 404 por un único @RestControllerAdvice, usando el ProblemDetail que Spring ya trae de serie como cuerpo del error en vez de inventar un tipo nuevo. Por la misma razón, los propios Cart y Product —records simples— hacen de request y de response directamente; no hay DTOs en ningún punto de la API, porque ningún endpoint necesitaba una forma distinta de la que el dominio ya tenía.

Verificarlo con tests

Cada capa se testea a su propio nivel. El controller, con @WebMvcTest y MockMvc, mockeando el servicio: lo que importa ahí es el contrato HTTP, no la lógica de negocio. El servicio y el scheduler, con JUnit 5 y Mockito sobre el repositorio mockeado: lo que importa es la regla (generar el id, fusionar productos, lanzar CartNotFoundException cuando toca, calcular qué carritos llevan más de diez minutos inactivos), no de dónde vienen los datos. Y el repositorio en memoria, con un test directo sobre la implementación real, sin mocks, porque ahí lo que quiero comprobar es que el ConcurrentHashMap guarda, busca y expira correctamente.

El resultado es una suite de 17 tests, ejecutada con ./mvnw test, que cubre las cuatro operaciones, la expiración por inactividad, y tanto el caso feliz como el de carrito inexistente en cada una de las tres operaciones que lo necesitan.

Dejarlo documentado

En el README.md quedó el enunciado completo (el PDF original se elimina del proyecto al final), la solución adoptada y por qué, la estructura de paquetes, cómo levantar el proyecto con el wrapper de Maven incluido, cómo ejecutar los tests, la API completa con ejemplos, y las limitaciones que asumí conscientemente: sin base de datos real, sin librería de validación de entrada más allá del propio tipado de Product. Nada de eso es un descuido; es la otra cara de mantener las dependencias al mínimo que pedía el enunciado.

El resultado

Un servicio pequeño, con la forma de la API decidida por el contrato de cada operación y no por el dominio, la ausencia de "carrito no encontrado" resuelta en un único punto en vez de repetida en cada flujo, y sin más piezas —DTOs, capas, dependencias— de las que el problema pedía.