Freestyler (Boomfunk MC)
Introducción
En un mundo ideal, todos comenzaríamos los proyectos desde cero, podríamos elegir las tecnologías que consideráramos más apropiadas para cada caso, los jefes nos entenderían, nosotros entenderíamos a los jefes, etc. En fin, un Mundo Feliz como el de Aldous Huxley. Sin embargo, para todo el que lleva algo de tiempo en el mundo del desarrollo de software (más o menos con su primer día de trabajo después acabar los estudios ya es suficiente) sabe lo que le toca: mantener código. Según lo veo yo, a esta actividad normalmente se la considera de segundo orden, y no lo es. La confusión es que muchos creen que mantener código pasa casi exclusivamente por cambiar cosas ya hechas o arreglar errores en el código, ya sea tuyo o de otros. Vamos, un sufrimiento. Pero mantener código es mucho más que eso, ya que la propia expresión cambiar cosas ya hechas es demasiado general. Puede abarcar desde cambios en la interfaz de usuario, añadir funcionalidad nueva a la ya existente, eliminar funcionalidad no requerida, mejora del rendimiento, separación y creación de componentes a partir del código actual para ser reutilizado, cambios en el sistema de construcción o despliegue de la aplicación, etcétera etcétera
Como se puede observar, las habilidades requeridas pueden ser muy heterogéneas y abarcar varios campos, e incluso podría decir que es hasta fascinante. Porque al fin y al cabo, la gran mayoría de las aplicaciones evolucionan una vez implantadas hasta que ya no son necesarias, y alguien tiene que hacerse cargo de esa evolución.
Mantenimiento en la Práctica
Muchas veces nos encontramos con código más o menos antiguo, al cual tenemos que añadir funcionalidad nueva o modificar la existente. El problema es que este código es totalmente nuevo para nosotros y no queremos romper nada. Normalmente esto sucede debido a un fuerte acoplamiento entre los componentes del sistema, en un sentido más o menos general: clases, métodos, etc. Un cambio simple en la implementación de un método de una clase puede afectar, transitivamente, a una clase de la que ni siquiera tenemos constancia de su existencia. Puede que el comportamiento erróneo introducido lo descubramos cuando la aplicación está en ejecución, en entornos de QA o Producción. Dependiendo de la tolerancia del proyecto al ciclo de feedback, podemos encontrarnos con una situación inaceptable. Y he aquí la cuestión principal a tener en cuenta: ¿cómo podemos saber si hemos introducido efectos colaterales no deseados en otras partes del sistema? Evidentemente, un sistema software actual puede llegar a ser muy complejo, pero existen formas de mitigar los efectos no deseados de nuestras modificaciones al código. En concreto, estoy hablando de los tests, tanto unitarios como de integración en este caso. Si tenemos un conjunto de tests que prueban la corrección del código existente, podremos sentirnos más seguros a la hora de añadir o modificar funcionalidad. Es por eso que la recomendación en este caso es la de crear ANTES tests para el código sobre el que debemos trabajar. Nos lo agradeceremos nosotros mismos, y nos lo agradecerá el resto del equipo.
Dependencias
Después de haber intentado dejar claro que un buen juego de tests son una especie de seguro de vida, vayamos a su vez con uno de los problemas que nos encontramos cuando queremos crear un test unitario para una clase de este sistema: las dependencias. Algo tan simple como este código puede ser un obstáculo tremendo para acometer lo que nos proponemos:
class A
{
private B b = new B();
public void doWhatever()
{
b.doBTask();
}
...
}
La clase A
tiene una relación de dependencia con la clase B
, lo que significa que en el método doWhatever()
de A
se utiliza la funcionalidad de B
. Si la dependencia fuera de uso estaríamos en un caso parecido. Ahora, si queremos crear un test unitario para dicho método de A
, estaremos indirectamente invocando al método doBTask()
de B
. En dos palabras, ya no sería un test unitario. Por tanto, centremos nuestro objetivo en el título de este post: romper dependencias. Aunque primero presentemos un ejemplo un poco más real, que a veces eso de tener clases A
y B
, más que ayudar, complica la explicación.
De camiones, ruedas y motores
Imaginemos un sistema de información de una empresa de transportes. Cada cierto período de tiempo, esta empresa realiza exhaustivos controles mecánicos a su flota de camiones. Esta podría ser la parte del sistema que nos interesa (obviamente muy simplista):
La clase Truck
podría tomar la siguiente forma en el código:
class Truck
{
private Wheel[] wheels;
private Engine engine; public Truck()
{
wheels = new Wheel[4]{new Wheel(), new Wheel(), new Wheel(), new Wheel()};
engine = new Engine();
}
public boolean review()
{
boolean firstWheelOK = wheels[0].check();
boolean secondWheelOK = wheels[1].check();
boolean thirdWheelOK = wheels[2].check();
boolean fourthWheelOK = wheels[3].check();
boolean engineOK = engine.check();
return (engineOK && firstWheelOK && secondWheelOK && thirdEngineOK && fourthEngineOK);
}
}
La clase anterior, evidentemente, es mejorable en muchos aspectos: ¿4 ruedas fijas?, ¿clase inmutable?, etc. Pero a efectos de ilustrar la rotura parcial de dependencias, nos sirve. Vayamos con el test. Esta técnica es independiente del framework de testing que utilicemos (JUnit en este ejemplo).
class TruckTest extends Test
{
private Truck truck;
public void setUp() throws Exception
{
truck = new Truck();
}
public void testCheck() throws Exception
{
assertTrue(truck.review());
}
}
La llamada truck.review()
, implica que se llaman los métodos check()
de las ruedas y del motor del camión. Esto sería correcto para un test de integración, pero no para uno unitario. ¿Qué podemos hacer? Pues bien, para todos aquellos afortunados que en su empresa utilizan un framework de IoC, podéis dar por finalizada la lectura de esto aquí. Delegar en Spring por ejemplo la creación y asignación de los objetos miembros de la clase Truck
(wheels y engine) es ciertamente algo deseable. Sin embargo, si una aplicación no hace uso de la inversión del control, ya sea por antigüedad o por la razón que sea, mi predicción es que en muchos casos migrar el código para delegar el ciclo de vida de las dependencias a un framework externo como Spring puede ser complicado y producir más de un dolor de cabeza. Así que valgámonos de una propiedad de la OO como es el polimorfismo para crear un test unitario para la clase Truck
.
El primer paso va a ser refactorizar la clase para que obtenga las referencias de sus miembros a través de un método accesor (getWheels()
y getEngine()
):
class Truck
{
private Wheel[] wheels;
private Engine engine; public Truck()
{
wheels = getWheels();
engine = getEngine();
}
public boolean review()
{
boolean firstWheelOK = wheels[0].check();
boolean secondWheelOK = wheels[1].check();
boolean thirdWheelOK = wheels[2].check();
boolean fourthWheelOK = wheels[3].check();
boolean engineOK = engine.check();
return (engineOK && firstWheelOK && secondWheelOK && thirdEngineOK && fourthEngineOK);
}
protected Wheel[] getWheels()
{
return new Wheel[4]{new Wheel(), new Wheel(), new Wheel(), new Wheel()};
}
protected Engine getEngine()
{
return new Engine();
}
}
El segundo paso va a ser crear clases mock de dichas dependencias, de forma que estáticamente podemos definir el comportamiento de éstas y poder así probar la clase Truck
en base a este comportamiento definido:
class TruckTest extends Test
{
private Truck truck; public void setUp() throws Exception
{
truck = new MockTruck();
}
public void testCheck() throws Exception
{
assertTrue(truck.review());
}
protected class MockTruck extends Truck
{
protected Wheel[] getWheels()
{
/* 1 */
create and return my own wheels!!!
}
protected Engine getEngine()
{
/* 2 */
create and return my own engine!!!
}
}
}
Como se puede ver, la clase interna MockTruck
es idéntica en comportamiento y estructura a la clase Truck
, excepto que sobreescribe los métodos accesores de las dependencias creando y devolviendo las ruedas y el motor cuyos estados definimos nosotros (/* 1 */ y /* 2 */). Con esto hemos conseguido aislar parcialmente la creación dichas dependencias, por lo tanto los tests sobre la clase Truck
que definamos suponen un estado y comportamiendo fijo de dichas dependencias. Hemos aislado parcialmente a Truck
y ahora sí que TruckTest
puede considerarse un test unitario.
Conclusiones
Un problema de uilizar esta técnica de cara al futuro puede ser que queramos crear tests de Truck
para diferentes estados de sus dependencias. Tendríamos que crear una clase MockTruck
por cada juego de comportamientos, o externalizar la creación de dichas dependencias en los accesores de la clase interna MockTruck
, o podríamos … En fin, varias posibilidades. Por supuesto, existen frameworks muy útiles para crear mocks dinámicamente, como jMock o rMock, pero puede ser también complicada su introducción en un sistema no concebido inicialmente para su testing unitario. Por ejemplo, si no se ha seguido la máxima programar contra interfaces, esto es, que no existan o que se programe a través de ellas. Pero vamos, que se decida lo que se decida son respecto al código legado, es preferible tener al menos algo como lo de arriba, a no tener anda.
Algo como Mantenimiento Dirigido por Test, puestos a aportar algo a todo ese océano de siglas con que nos abruman cada día a los programadores.