mobile menu icon

La kata Gilded Rose en PL/SQL: escribiendo los tests

Publicado por Fran Reyes el 07/07/2019

Testing, PL/SQL, Katas, Refactoring


Contexto.

En uno de nuestros actuales clientes, Mutua Tinerfeña, estamos trabajando diferentes técnicas para construir software de manera progresiva y confiable. Aunque el equipo es pequeño, sus miembros usan y conocen tecnologías muy diferentes, por lo que necesitábamos practicar dichas técnicas usando como vehículo un lenguaje que dominaran todos los miembros del equipo. Con esto podíamos evitar que algunos miembros del equipo tuvieran que aprender otros paradigmas para poder practicar las nuevas técnicas. Así que elegimos PL/SQL, que era el lenguaje que todos tenían en común, como vehículo de aprendizaje.

Este ejercicio es parte de un curso sobre refactoring de base de datos que estamos preparando con mucho amor. Este curso está pensado para equipos que trabajan con un legacy en el que la mayoría del código se encuentra en la base de datos y puedan incorporar técnicas como testing y empezar a trabajar su legacy con confianza y de forma más sostenible. Esta primera versión del curso está orientado fundamentalmente a Oracle como SGBD, aunque muchas de las ideas pueden ser trasladadas a otros SGBD como SQLServer.

Aprendiendo refactoring y TDD en PL/SQL

Una de las prácticas que hicimos fue resolver la kata Gilded Rose en PL/SQL para practicar refactoring y TDD. En esta kata lo primero que se debe hacer, antes de añadir la funcionalidad que nos piden, es cubrir el código de tests. Estos tests nos permiten refactorizar el código para hacer que, finalmente, sea fácil añadir la nueva funcionalidad usando TDD. En este post contaremos cómo escribimos los tests para la versión PL/SQL de la kata.

Testeando la kata Gilded Rose en PL/SQL usando utPSQL

La herramienta que usamos para testear el código PL/SQL fue utPSQL que es un framework de testing open-source para PL/SQL and SQL. utPSQL nos permite lanzar los tests de manera muy fácil.

Para escribir los tests hay que crear un paquete[1]. En la especificación del paquete hay que añadir una serie de anotaciones, y, por último, escribir el propio test en el cuerpo del paquete.

Para lanzar todos los tests desde la base de datos[2] hay que hacer la siguiente llamada a la “librería”:

begin ut.run(); end;

Esta llamada buscará en el schema todos los paquetes que contengan las anotaciones y lanzará sus tests. Existe también la posibilidad de lanzar sólo los tests de un determinado paquete indicándolo como un argumento.

Estos son los tests para la kata Gilded Rose en PL/SQL:

CREATE OR REPLACE PACKAGE test_update_quality
IS
--%suite(Update quality)
--%beforeeach
PROCEDURE setup;
--%test(sell in decreases every day)
PROCEDURE sell_in_decreases_every_day;
--%test(quality decreases every day)
PROCEDURE quality_decreases_every_day;
--%test(quality decreases twice as fast one sell in has passed)
PROCEDURE quality_decreases_twice;
--%test(quality is never negative)
PROCEDURE quality_is_never_negative;
--%test(aged brie increases quality)
PROCEDURE aged_brie_increases_quality;
--%test(item never increase quality when has reached the maximum)
PROCEDURE quality_has_a_maximun;
--%test(sulfuras never changes the quality)
PROCEDURE sulfuras_never_changes_quality;
--%test(sulfuras never changes the sell in)
PROCEDURE sulfuras_never_changes_sell_in;
--%test(backstage increase quality)
PROCEDURE backstage_increase_quality;
--%test(backstage increase quality by 2 when sell in is 10 or less)
PROCEDURE bkstg_q_by_2_sellin_is_10_less;
--%test(backstage increase quality by 3 when sell in is 5 or less)
PROCEDURE bkstg_q_by_3_sellin_is_5_less;
--%test(backstage drops to 0 quality after the concert)
PROCEDURE bkstg_q_drops_0_after_concert;
--%test(aged brie expired gets the maximum quality when has the quality minus 1)
PROCEDURE aged_brie_expired_the_max_q;
END test_update_quality;
/
CREATE OR REPLACE PACKAGE BODY test_update_quality
IS
expired_sellin CONSTANT int := 0;
minimun_quality CONSTANT int := 0;
maximum_quality CONSTANT int := 50;
PROCEDURE expectQualityToBe(qualityExpected IN int) IS
quality item.quality%TYPE;
BEGIN
SELECT QUALITY INTO quality FROM item;
ut.expect(quality).to_equal(qualityExpected);
END;
PROCEDURE expectSellinToBe(sellinExpected IN int) IS
sell_in item.SELL_IN%TYPE;
BEGIN
SELECT SELL_IN INTO sell_in FROM item;
ut.expect(sell_in).to_equal(sellinExpected);
END;
PROCEDURE add_regular_product_with(sell_in item.sell_in%TYPE,
quality item.quality%TYPE)
IS
BEGIN
new_item('any_product', sell_in, quality);
END;
PROCEDURE add_aged_brie_with(sell_in item.sell_in%TYPE,
quality item.quality%TYPE)
IS
BEGIN
new_item('Aged Brie', sell_in, quality);
END;
PROCEDURE add_sulfuras_with(sell_in item.sell_in%TYPE,
quality item.quality%TYPE)
IS
BEGIN
new_item('Sulfuras, Hand of Ragnaros', sell_in, quality);
END;
PROCEDURE add_backstage_with(sell_in item.sell_in%TYPE,
quality item.quality%TYPE)
IS
BEGIN
new_item('Backstage passes to a TAFKAL80ETC concert', sell_in, quality);
END;
------------ TESTS ----------
PROCEDURE setup IS
BEGIN
DELETE FROM item;
END;
PROCEDURE sell_in_decreases_every_day
IS
BEGIN
add_regular_product_with(sell_in => 5, quality => 15);
update_quality();
expectSellinToBe(4);
END;
PROCEDURE quality_decreases_every_day
IS
BEGIN
add_regular_product_with(sell_in => 5, quality => 15);
update_quality();
expectQualityToBe(14);
END;
PROCEDURE quality_decreases_twice
IS
BEGIN
add_regular_product_with(sell_in => expired_sellin, quality => 15);
update_quality();
expectQualityToBe(13);
END;
PROCEDURE quality_is_never_negative
IS
BEGIN
add_regular_product_with(sell_in => expired_sellin, quality => minimun_quality);
update_quality();
expectQualityToBe(minimun_quality);
END;
PROCEDURE aged_brie_increases_quality
IS
BEGIN
add_aged_brie_with(sell_in => 5, quality => 15);
update_quality();
expectQualityToBe(16);
END;
PROCEDURE quality_has_a_maximun
IS
BEGIN
add_aged_brie_with(sell_in => 5, quality => maximum_quality);
update_quality();
expectQualityToBe(50);
END;
PROCEDURE sulfuras_never_changes_quality
IS
BEGIN
add_sulfuras_with(sell_in => 4, quality => 30);
update_quality();
expectQualityToBe(30);
END;
PROCEDURE sulfuras_never_changes_sell_in
IS
BEGIN
add_sulfuras_with(sell_in => 4, quality => 30);
update_quality();
expectSellinToBe(4);
END;
PROCEDURE backstage_increase_quality
IS
BEGIN
add_backstage_with(sell_in => 15, quality => 30);
update_quality();
expectQualityToBe(31);
END;
PROCEDURE bkstg_q_by_2_sellin_is_10_less
IS
BEGIN
add_backstage_with(sell_in => 10, quality => 30);
update_quality();
expectQualityToBe(32);
END;
PROCEDURE bkstg_q_by_3_sellin_is_5_less
IS
BEGIN
add_backstage_with(sell_in => 5, quality => 30);
update_quality();
expectQualityToBe(33);
END;
PROCEDURE bkstg_q_drops_0_after_concert
IS
BEGIN
add_backstage_with(sell_in => expired_sellin, quality => 30);
update_quality();
expectQualityToBe(0);
END;
PROCEDURE aged_brie_expired_the_max_q
IS
BEGIN
add_aged_brie_with(sell_in => expired_sellin, quality => maximum_quality - 1);
update_quality();
expectQualityToBe(maximum_quality);
END;
END test_update_quality;
/

Aunque pueda parecer sorprendente, los tests resultantes son bastante legibles. La legibilidad de los tests, en comparación con otras librerías similares, es uno de los puntos a favor de utPSQL.

Conclusiones

Hemos visto como es posible hacer testing en PL/SQL usando utPSQL. Estos tests crearán una red de seguridad que nos permitirá refactorizar el código, o lo que es lo mismo, mejorar su diseño preservando su comportamiento.

En el próximo post de esta serie enseñaremos como se pueden aplicar técnicas de refactoring y de diseño para mejorar el código PL/SQL de la kata Gilded Rose en pequeños pasos manteniendo los tests en verde en todo momento.

Agradecimientos

Me gustaría agradecer a mi compañero Manuel Rivero por ayudarme a revisar y editar este post, y al equipo de Mutua Tinerfeña por las aportaciones ofrecidas desde su amplia experiencia en este entorno.

[1] Un paquete es una agrupación lógica de procedimientos, funciones, tipos, constantes, etc. Un paquete tiene 2 partes, una especificación y un cuerpo. La especificación es una interfaz para el consumidor y el cuerpo expone la implementación.
[2] También existe la posibilidad de lanzar los tests desde una términal, lo que permite vincularlos a un sistema de integración continua de manera muy sencilla.
Volver a posts