Feeds:
Entradas
Comentarios

Archive for the ‘TDD’ Category


Hola a todos, aquí estoy de nuevo tras un tiempo de inactividad. La verdad es que andaba yo sin ningún tipo de inspiración para un nuevo post cuando el mundo real me ha obligado a escribir este. La verdad es que en estos meses, tanto en conversaciones, como en twitter, como en varios cursos de calidad, agilismo y testing que he impartido, han surgido de manera recurrente las siguientes preguntas: ¿y cómo engancho el selenium en todo esto? ¿Y cuándo grabo las navegaciones para automatizar las pruebas funcionales? ¿Y porque tanto rollo de BDD si ya tengo el producto “Super UI Test Automator Robot” que me graba las navegaciones? Y así una detrás de otra. Reconozco que si no te interesa el enfoque BDD ni el TDD, puedes seguir grabando navegaciones como modo de “automatizar test funcionales”. Para el resto de vosotros os ofrezco varias razones por las que considero que usar éste enfoque es una aberración:

La primera razón es bastante trivial, si haces BDD entonces tienes un enfoque “test first”, es decir, escribes el test antes que la implementación, y no se escribe ni una sola línea de código hasta que no tienes una especificación (test) en rojo. En este sentido es completamente imposible usar el paradigma de grabar navegaciones para automatizar el testing, ya que tienes que tener el test automatizado antes que la implementación y por lo tanto no puedes grabar nada.

La segunda razón tiene que ver con el mantenimiento. Si decidimos que el test de cada escenario de una historia de usuario se va a automatizar grabando una navegación, entonces lo que estamos testeando realmente es la UI y no la funcionalidad propiamente dicha. Cada vez que se produzca un cambio trivial en la UI de una página o panel, vamos a tener que grabar de nuevo todas las navegaciones que usen ese panel o página, aunque la funcionalidad no haya cambiado realmente. Claramente al usar el paradigma de “grabar navegación” estamos acoplando nuestros tests al diseño fino de la interfaz de usuario, que como todos sabemos, cambia con más frecuencia que la funcionalidad propiamente dicha de la aplicación. Un simple cambio en el atributo “name” o “id” de un elemento, o eliminar un botón de “buscar” para hacer una búsqueda en tiempo real, nos va a estropear las navegaciones.

Ojo, también nos podemos meter en este lío haciendo BDD, con por ejemplo Cucumber, si no tenemos cuidado. Pero mi argumento es que si usamos una herramienta que graba navegaciones como base de nuestra estrategia de testing funcional, este problema es inevitable. Por el contrario si decidimos usar un enfoque basado en programar nuestros tests podemos evitarlo fácilmente. ¿Cómo? Simplemente usando el patrón “page object”, e implementar cada “page object” con algún framework de automatización de UI, como WebDriver de Selenium o Watir. De esta forma si se produce un cambio en una UI, que no altere la funcionalidad, entonces sólo necesitamos retocar el “page object” correspondiente, sin necesidad de modificar nada en nuestros tests, escenarios y steps.

El que nuestros tests sean código hecho por nosotros es bueno. Podemos aplicar todas las técnicas de ingeniería del software que conocemos y aumentar la mantenibilidad de nuestros tests. Eso es, recordad que el código de test tiene que ser mantenible, y todo el tema de legibilidad, DRY y SOLID se le aplica, y por lo tanto podemos usar toda nuestra habilidad para que esto se cumpla.

Bien, y esto es todo, ¡ no diréis después que mis posts son muy largos ;-) !

P.S. Sois programadores, ¡ que no os asuste programar ! ¡ No os escondais en herramientas !

Read Full Post »


Andaba yo buscando un ejemplo de código para demostrar el concepto del TDD, refactor y diseño emergente, cuando mi señora me apuntó al coding dojo en Ruby, organizado por @madridrb el pasado día 30 de Diciembre, donde durante las cervezas post-evento se me ocurrió este post. El dojo, donde el señor @ecomba propuso hacer la kata de los números romanos, se encontraba con bastante más gente de lo normal, y es que este hombre tiene mucho tirón. La idea era ir turnándose cada 3 minutos en un ordenador e ir avanzando la kata. Como éramos bastantes, y no había mucho tiempo, al final realmente no pudimos avanzar lo suficiente, y tuvo que acabarla @ecomba de forma rápida y sin poder entretenerse mucho. Esto hizo que tuviera que dar algunos saltos de diseño. Posteriormente en las cervezas me enteré que muchos asistentes no estaban muy versados en esto del TDD y que estaban confusos. Sobre todo la mayor discusión trataba sobre por qué era importante hacer TDD y refactor en pasos muy pequeños y qué ventaja daba a la hora de diseñar el código. Así que en ese momento supe que mi siguiente post iba a ser un ejemplo con código sobre cómo conseguir un diseño emergente a base de Refactor y TDD en pasos pequeños (baby steps).

Este es uno de los posts más difíciles que he hecho ya que voy a intentar mostrar mis procesos mentales de diseño, y como me enfrento a un problema de programación (sencillo por cierto). Aviso a los asistentes del coding dojo que el código final al que llego en este post es diferente al que hizo @ecomba, lo que demuestra que no suele existir una única implementación “óptima”, y que el resultado puede variar en función de criterios personales y algo del azar. No tengo experiencia alguna en Ruby, sólo he hecho los koans y esta kata, con lo que he usado un Ruby muy básico y sencillo al no conocer bien el lenguaje y la librería de objetos. Sin embargo guiado por el TDD y el refactor creo que llego a una solución bastante compacta. Empecemos…

Lo primero de todo es familiarizarse con la funcionalidad a implementar. Ni el TDD ni el refactor son un sustituto de ponerse a pensar, sólo un guía para nuestro proceso creativo, por lo que debemos tener alguna idea del objetivo funcional que debe alcanzar nuestro código. No es necesario ser un experto en la funcionalidad, sino saber por donde van más o menos los tiros y no ir dando palos de ciego. Aquí va la funcionalidad de los números romanos, al menos lo que recordaba cuando me puse a tirar código:

  • Queremos convertir un número entero en un número romano. Nada más.
  • Los romanos no representaban de forma explícita ni el cero ni los números negativos.
  • Existen un conjunto de 13 símbolos o numerales básicos (nosotros tenemos 10). Cada uno de estos tiene un valor predefinido, pero ninguno representa el 0 o valor negativo.
  • Para representar un número, se concatenan estos numerales, y sus valores se van sumando hasta que se obtiene el valor del número.
  • Algunos casos como el 4 o el 9 son especiales y se representan de forma especial.

Así que llegué a mi casa, abrí el vim, me instalé el rspec y secuestré el “mug of vi” de mi mujer para que me sirviera de chuleta (soy penoso con el vi y quería aprender, que lo pasé muy mal en el dojo). Lo primero que se hace siempre es coger el caso más básico, y que cosa más básica que el número 1. En rspec queda:

describe "Roman number" do
	it "I is equivalent to 1" do
		1.to_roman.should == 'I'
	end
end

Evidentemente falla, con lo que hay que añadir una implementación. Pero siguiendo las reglas del TDD y refactor, debe ser la implementación más simple posible que pase el test, ya lo pondremos bonito después si existiera una razón de peso. Para los que no sepan Ruby, en éste lenguaje las clases son abiertas, por lo que el enfoque de diseño tomado es añadir un método “to_roman” a la clase “Fixnum” que representa a los enteros.  La implementación es obvia:

class Fixnum
	def to_roman
		'I'
	end
end

describe "Roman number" do
	it "I is equivalent to 1" do
		1.to_roman.should == 'I'
	end
end

Vamos a probar ahora con el número 5, que es otro de los numerales básicos romanos. La estrategia de momento es ir añadiendo numerales básicos, a ver que nos sale. El test y su correspondiente implementación:

class Fixnum
	def to_roman
		return 'V' if self == 5
		'I'
	end
end

describe "Roman number" do
	it "I is equivalent to 1" do
		1.to_roman.should == 'I'
	end

	it "V is equivalent to 5" do
		5.to_roman.should == 'V'
	end
end

La técnica es sencilla, copiar y pegar lo que funcionó en el caso del 1. Reemplazamos los valores adecuados y cubrimos cada caso con un if. Estamos en modo “pasar tests”, hasta el momento no hemos detectado duplicación y no hemos refactorizado nada. Pero esto ya empieza a oler a cuerno quemado. Volvemos a repetir, esta vez para el 10. La cosa queda:

class Fixnum
	def to_roman
		return 'X' if self == 10
		return 'V' if self == 5
		'I'
	end
end

describe "Roman number" do
	it "I is equivalent to 1" do
		1.to_roman.should == 'I'
	end

	it "V is equivalent to 5" do
		5.to_roman.should == 'V'
	end

	it "X is equivalent to 10" do
		10.to_roman.should == 'X'
	end
end

Ya se observa claramente la duplicación. Tenemos dos (o tres, según se mire) lineas con la misma estructura de código. Esto es normal ya que hemos estado haciendo copy&paste. Ahora toca reflexionar sobre la intención de nuestro código, ¿qué queremos hacer en realidad? ¿Refleja el código actual esa intención (es expresivo)? Es obvio que lo que queremos hacer es mapear números a numerales romanos, y que existe una correspondencia uno a uno. En realidad estamos haciendo una búsqueda de un literal romano por número entero a base de un montón de ifs. El uso de una simple Hash (o mapa o diccionario) nos elimina las líneas duplicadas y nos da un código más expresivo (si elegimos bien los nombres). Tras refactorizar me sale lo siguiente:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		ARABIC_TO_ROMAN_NUMERAL[self]
	end
end
# .......

Por brevedad no me entretendré con los siguientes 10 numerales romanos, y pasaré al siguiente problema, ¿qué pasa si un número no se corresponde con un literal romano? Según el funcional hay que concatenar los numerales hasta sumar el número deseado. Como caso más sencillo de este escenario añado el test para el número 2 que debe transformarse en ‘II’. Además voy a refactorizar un poco los ejemplos de test, agrupándolos por escenario. En RSpec usaré un contexto por escenario (no se si esto es purista pero a mi me parece bien). El test ahora es:

# .........
describe "Roman number" do
	context "has basic numerals with different values" do
		it "I is equivalent to 1" do
			1.to_roman.should == 'I'
		end

		it "V is equivalent to 5" do
			5.to_roman.should == 'V'
		end

		it "X is equivalent to 10" do
			10.to_roman.should == 'X'
		end
	end

	context "concatenates numerals in descending order until they sum up the desired integer" do
		it "II is equivalent to 2" do
			2.to_roman.should == 'II'
		end
	end
end

Como veis un contexto para los numerales básicos y otro para los que no lo son. Añado la implementación más básica que se me ocurre, si el número coincide con un numeral básico lo devuelvo y acabo, si no, devuelvo ‘II’ a cascoporro:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		return ARABIC_TO_ROMAN_NUMERAL[self] if ARABIC_TO_ROMAN_NUMERAL[self]
		'II'
	end
end
# ........

Añado un test para el número 3, que debe resultar en ‘III’ y su correspondiente implementación. Pero esta vez me lo curro un poco más:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		return ARABIC_TO_ROMAN_NUMERAL[self] if ARABIC_TO_ROMAN_NUMERAL[self]
		'I' + (self - 1).to_roman
	end
end

describe "Roman number" do

#      ......................

	context "concatenates numerals in descending order until they sum up the desired integer" do
		it "II is equivalent to 2" do
			2.to_roman.should == 'II'
		end

		it "III is equivalent to 3" do
			3.to_roman.should == 'III'
		end
	end
end

Como veis esta vez he sido más sofisticado y en vez de meter un ‘III’ if self == 3, he leído bien el funcional y se me ha ocurrido un algoritmo. Siguiendo el espíritu de concatenar hasta sumar el número, se me ocurre que puedo tomar el numeral ‘I’ y restarle su valor al número que quiero convertir. El resultado de esta resta, qué es lo que queda para conseguir sumar el número deseado, lo convierto a su vez en un número romano y lo concateno. Aquí hemos tenido que parar y reflexionar sobre la funcionalidad para obtener una idea creativa. El TDD nos ha servido para llegar a un punto donde esta idea se nos pueda ocurrir con facilidad.

Sigamos, ¿qué pasa con otros casos? Añado tests para el 6, el 11 el 15 y el 20. Los hago pasar haciendo copy&paste del caso del 2 y el 3, reemplazando ‘I’ y 1, por ‘V’ y 5 para pasar el test del 11, y ‘X’ y 10 para pasar el test del número 9. La cosa queda así:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		return ARABIC_TO_ROMAN_NUMERAL[self] if ARABIC_TO_ROMAN_NUMERAL[self]
		return 'X' + (self - 10).to_roman if self > 10
		return 'V' + (self - 5).to_roman if self > 5
		'I' + (self - 1).to_roman
	end
end

describe "Roman number" do

#      .................

	context "concatenates numerals in descending order until they sum up the desired integer" do
		it "II is equivalent to 2" do
			2.to_roman.should == 'II'
		end

		it "III is equivalent to 3" do
			3.to_roman.should == 'III'
		end

		it "VI is equivalent to 6" do
			6.to_roman.should == 'VI'
		end

		it "XI is equivalent to 11" do
			11.to_roman.should == 'XI'
		end

		it "XV is equivalent to 15" do
			15.to_roman.should == 'XV'
		end

		it "XX is equivalent to 20" do
			20.to_roman.should == 'XX'
		end
	end
end

Fijaros en el orden en que pongo las líneas de código, primero evalúo los numerales con valor más alto y después las de valor más bajo. Si lo hacemos en otro orden los tests fallan, al devolverme por ejemplo ‘VVI’ en vez de ‘XI’. En este caso los tests son los que nos han hecho darnos cuenta de que hay que ordenar los numerales de mayor a menor, y además de sumar el valor, debe ser la representación más corta posible. Esto no lo tenía yo nada claro por el funcional que indiqué más arriba. En este caso los tests nos aclaran la funcionalidad.

Sin embargo de nuevo tenemos duplicación y el código da repelús. La misma estructura de código repetida en tres líneas, sólo varía en el numeral romano usado en cada caso y su correspondiente valor numérico. ¿Podemos sustituir esta duplicación por una regla parametrizable? ¿Quizás un método auxiliar que recibiera como parámetros el numeral romano y el valor numérico? Esta solución eliminaría algo de duplicación, pero aun quedaría duplicada la estructura de la cascada de ifs. Es esto último lo que más me preocupa, ya que se viola el principio abierto/cerrado. Cuando quisiéramos añadir un nuevo numeral (cuando nuestro cliente recordara uno nuevo), tendríamos que “abrir” el método to_roman para añadir otra línea más. Esto es más grave que unos cuantos caracteres repetidos. Hay que pararse a pensar de nuevo, es hora de ganarse el sueldo. Lo que se me ocurrió es lo siguiente:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		return ARABIC_TO_ROMAN_NUMERAL[self] if ARABIC_TO_ROMAN_NUMERAL[self]
		ARABIC_TO_ROMAN_NUMERAL.each do | arabic_number, roman_numeral |
			return roman_numeral + (self - arabic_number).to_roman if self > arabic_number
		end
	end
end
#  .............

La idea es recorrer la colección de numerales, de forma que encontremos el numeral romano de más valor, que sea inferior al número que buscamos, y usamos dicho numeral en la regla recursiva que descubrimos antes para calcular el resultado. Podríamos haber usado un bucle for, pero sospecho que no están bien vistos en el mundo de Ruby, así que uso el método iterador each, y le paso un bloque de código. En cuanto encuentro el numeral buscado devuelvo el valor y el bucle (perdón, la iteración) termina. De paso ya no necesito ese método auxiliar que se me ocurrió antes, ya que la regla recursiva sólo se usa una vez y es bastante compacta. Me encanta cuando los planes salen bien… WTF! ¡ Fallan los tests ! ¡ Qué c*** pasa ! ¡ Es la hora del debugger !

Lo que ocurre es que el método each itera las entradas de la hash en el orden que le sale de las gónadas. Es una sorpresa, ya que según la documentación, debería iterarlos en el orden en que los añades a la hash. Ya estoy por mandar un bug a la comunidad de Ruby cuando descubro mi fallo. Estoy usando Ruby 1.8.x y la documentación es de la 1.9.x. Algo huele a quemado, busco un poco y efectivamente: originalmente el orden de iteración de las hash era aleatorio, pero a partir de la 1.9.x es por orden de inserción. Esto de añadir cambios que rompen la API y cambiar sólo el “minor version” no es buena idea.

Total, que tengo una implementación que supuestamente funciona en Ruby 1.9 (no lo he probado), pero no en 1.8. Algo he de hacer. De momento me calmo un poco, y me dedico a arreglar otra duplicación de código que me está matando, la primera línea del método to_roman. Lo que hago es lo siguiente:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }

	def to_roman
		return '' if self == 0
		ARABIC_TO_ROMAN_NUMERAL.each do | arabic_number, roman_numeral |
			return roman_numeral + (self - arabic_number).to_roman if self >= arabic_number
		end
	end
end
# ..............

Directamente elimino la línea y cambio self>arabic_number por self>=arabic_number. La idea es que si encuentro un numeral que sea exactamente igual al valor buscado, también puedo aplicar la regla recursiva. En este caso el resto es 0, que como sabemos no se representa en números romanos. Se soluciona devolviendo cadena vacía como caso base de la recursividad cuando el número es 0, eliminando la fea duplicación que teníamos como caso base.

Ahora ya puedo centrarme en hacerlo retrocompatible con Ruby 1.8. Simplemente ordeno de forma explícita la hash en orden descendiente (de mayor a menor). Un cambio trivial:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 5 => 'V', 1 => 'I' }.sort.reverse

# ................

end

Dicho sea de paso, esta ordenación me devuelve un array, con lo que en vez de una hash termino con un array de pares clave/valor en la constante ARABIC_TO_ROMAN_NUMERAL. Debido a que itero con el método each, y a que realmente había dejado de usar la búsqueda por clave, el cambio es transparente.

Ahora toca enfrentarse a la tercera fase de la especificación funcional: los casos especiales. Los romanos no escribían ‘IIII’ para el número 4, sino ‘IV’. Lo mismo con el 9 que se representa como ‘IX’ en vez de ‘VIIII’ (esta regla es para los números romanos clásicos, la versión más primitiva no la tenía). Añado estos ejemplos y obviamente los tests fallan. Tal vez tenga que hacer un algoritmo de simplificación, de modo que si hay tres numerales seguidos iguales los sustituya por la versión simplificada. Parece difícil. Antes de complicarme la vida, y por si cuela, se me ocurre añadir ‘IX’ y ‘IV’ a los numerales básicos. Sorprendentemente funciona, me olvido de algoritmos complicados. Finalmente añado algunos tests con números complicados, para asegurarme que todo funciona bien. El código final es:

class Fixnum
	ARABIC_TO_ROMAN_NUMERAL = { 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I' }.sort.reverse

	def to_roman
		return '' if self == 0
		ARABIC_TO_ROMAN_NUMERAL.each do | arabic_number, roman_numeral |
			return roman_numeral + (self - arabic_number).to_roman if self >= arabic_number
		end
	end
end

describe "Roman number" do
	context "has basic numerals with different values" do
		it "I is equivalent to 1" do
			1.to_roman.should == 'I'
		end

		it "IV is equivalent to 4" do
			4.to_roman.should == 'IV'
		end

		it "V is equivalent to 5" do
			5.to_roman.should == 'V'
		end

		it "IX is equivalent to 9" do
			9.to_roman.should == 'IX'
		end

		it "X is equivalent to 10" do
			10.to_roman.should == 'X'
		end
	end

	context "concatenates numerals in descending order until they sum up the desired integer" do
		it "II is equivalent to 2" do
			2.to_roman.should == 'II'
		end

		it "III is equivalent to 3" do
			3.to_roman.should == 'III'
		end

		it "VI is equivalent to 6" do
			6.to_roman.should == 'VI'
		end

		it "XI is equivalent to 11" do
			11.to_roman.should == 'XI'
		end

		it "XV is equivalent to 15" do
			15.to_roman.should == 'XV'
		end

		it "XX is equivalent to 20" do
			20.to_roman.should == 'XX'
		end
	end

	context "converts even complex examples (to gain more trust in our implementation)" do
		it "XVIII is equivalent to 18" do
			18.to_roman.should == 'XVIII'
		end

		it "XIX is equivalent to 19" do
			19.to_roman.should == 'XIX'
		end

		it "XXXVII is equivalent to 37" do
			37.to_roman.should == 'XXXVII'
		end
	end
end

Como veis el código es diferente al que propuso @ecomba. Curiosamente @ialcazar me mostró la solución de @cavalle que es bastante similar a la mía, seguramente porque ambos hemos optado por un enfoque recursivo. Eso sí, ninguna de las tres soluciones tiene métodos de más de 4 líneas de código ;-). Notad que he tomado dos decisiones de diseño importantes: me he decidido por un diseño recursivo, y los casos especiales, como ‘IV’ o ‘IX’, los trato como numerales básicos. ¿Qué ocurriría si hubiéramos decidido que los casos especiales no son numerales básicos? ¿A alguien le interesa explorar este camino?

¿Para que nos ha servido el TDD? En este caso nos ha servido para guiar el proceso de pensamiento. Añadiendo código poco a poco puedo detectar duplicación, violaciones de principios SOLID, y otros problemas rápidamente. Es en estos momentos donde TDD+Refactor en pasos pequeños, nos obliga a parar y pensar. Hasta que no tengamos una visión más profunda del problema, no podremos avanzar, y ésta forma de trabajar nos golpea en la cabeza obligándonos a reflexionar. Sin embargo no olvidemos que el problema de los números romanos es pura algorítmica. En casos más complejos como diseño OO el TDD brilla en todo su esplendor.

Si queréis, podéis entrar github y echarle un vistazo a todo el histórico de “baby steps” que fui haciendo. ¿Alguien se anima a hacerlo en otro lenguaje?

Read Full Post »


Hola, seguimos con la polémica del TDD, pero no temais, en este post no voy a hablar de TDD (al menos no mucho). En el anterior post defendí que si se hacía bien el TDD, con su refactor y que si además se complementaba con un pair programming, la necesidad de métricas estáticas de calidad de código desaparecen. Comentarios a dicho post por parte de @jmbeas, Chuidiang e @ydarias indicaban que mi postura era correcta en un equipo experto, pero que si tenías equipos menos maduros, entonces necesitabas estas métricas porque los equipos no iban a hacer TDD, refactor y pair programming, ya sea por inexperiencia o por simplemente no querer. Bien, esto es cierto, de hecho es interesante que al enseñar TDD y refactor a equipos novatos,  usemos análisis de código estático para ver como de forma incremental, y emergente, el código alcanza una buena calidad.

En el fondo, mi problema es que pienso que los equipos que no hacen TDD o pair programming, sencillamente no están haciendo agilismo… Enrique, no te metas en más líos, mejor digamos que sencillamente no están usando una buena metodología. En el resto de este post voy a explicar tan inverosímil afirmación.

El agilismo propone un metaproceso para definir un proceso de desarrollo e ir evolucionándolo en el tiempo, con el objetivo de adaptarnos a los cambios en nuestro entorno y mejorar dicho proceso de forma continua, de manera que maximicemos el valor entregado al cliente. No vale con que el proceso sea bueno hoy, sino que debe ser mejor mañana y no debe quedarse obsoleto. Y por mejor entendemos que entregamos más valor al cliente en menos tiempo y con menos coste. Obviamente, para poder implementar el proceso de desarrollo en la realidad, y poder hacer mejora continua, se nos propone un conjunto de buenas prácticas. Mi pregunta es, ¿cuál es el conjunto mínimo de buenas prácticas para considerarnos ágiles? Dicho de otra forma, ¿cuál es la metodología ágil más ligera posible? Dada la definición de agilismo anteriormente expuesta, la metodología ágil más ligera, es aquella que sólo obliga prácticas que proporcionen lo más rápidamente posible información de los problemas del proyecto, y nos ayuden a resolverlos cuanto antes. Armados con unos ciclos de feedback rápidos, que nos informen sobre problemas en el proyecto, podemos practicar mejora continua, y terminaremos con una metodología que es perfecta para nuestro entorno de trabajo específico, y que va a evolucionar para adaptarse a cualquier imprevisto.

Dicho esto, hay que tener en cuenta que los problemas nos lo podemos encontrar a múltiples niveles: estimación y planificación, desarrollo, build y pases de entornos, explotación, etc. Necesitamos pues, ciclos de feedback y mejora continua en cada uno de esos niveles. Obviamente algunos de dichos niveles escapan a mi control, pero al menos planificación y desarrollo sí están bajo mi control. Al conjunto mínimo de prácticas, que te permiten conseguir mejora continua de forma efectiva, lo llamaré agilismo minimalista.

A nivel de control de proyecto y mejora continua lo mínimo que puedes hacer es:

  • Retrospectiva al final de proyecto. Hay que reunirse para saber las causas del éxito o fracaso del proyecto, y que cosas se pueden mejorar y cómo. Es el nivel básico de mejora, aprender del resultado de un proyecto para el siguiente. Debería participar la dirección de la empresa también y el cliente.
  • Múltiples reuniones de seguimiento del proyecto, a ser posible a intervalos regulares, predecibles y ni demasiado largos ni cortos. Un ejemplo de reunión de seguimiento es la retrospectiva de sprint dentro de SCRUM. Otro ejemplo es la demo de final de sprint, este último más orientado a obtener feedback del cliente. Nos permite enterarnos rápidamente de los problemas que se producen durante el proyecto sin esperar a que acabe. Si hay algún problema podemos tener la oportunidad de solucionarlo. También podemos ajustar nuestras estimaciones con la realidad del proyecto, y aprender a estimar mejor en el futuro. Se puede involucrar a todos los participantes en el proyecto, al fin y al cabo no se hace todos los días. Esto es importante para que la información del estado del proyecto llegue a todo el mundo por igual y no se quede información escondida en nichos. También debe participar un representante del cliente, para ver como va el proyecto, proporcionar información adicional y aclarar dudas.
  • Reunión diaria o daily scrum. Es un nivel de feedback más rápido, diario, que involucra a los miembros de un mismo equipo y al menos un responsable. En proyectos grandes pueden haber varios equipos cada uno con su propia reunión diaria.
  • Reunión de emergencia. Para solventar cualquier problema grave y no previsto, detectado mediante cualquier mecanismo.

A nivel de planificación y estimación:

  • Dividir el trabajo en unidades manejables. Dichas unidades deben ser estimables, representar un incremento en el valor entregado al cliente, concisas y claras, de un tamaño similar entre si, independientes entre si, tener un criterio de aceptación y no demasiado grandes. Esto se suele conocer como historias de usuario. La importancia de las historias de usuario es que te proporcionan una unidad de planificación y estimación que está bien correlacionada con el valor del proyecto desde el punto de vista del cliente. El criterio de aceptación es importante para saber cuando se ha terminado de implementar la historia.
  • Revisa las estimaciones frecuentemente, teniendo en cuenta los resultados obtenidos en las retrospectivas y reuniones anteriormente mencionadas. Si las historias no fueran pequeñas, independientes y de tamaños similares, el ajuste de las estimaciones basándonos en lo que pasó en otras historias sería prácticamente imposible.
  • Refina y revisa la definición de las historias frecuentemente. Tal vez a intervalos regulares (SCRUM sprint planning) o tan pronto como te sea posible (kanban). Para ello hay que hablar con el propio usuario, o en su defecto con un experto de negocio.
  • Implementa primero las historias con más valor. Esto maximiza el valor entregado al cliente. También nos evita tener que implementar historias que pueden cambiar o quedar obsoletas con el transcurso del tiempo.

De momento a nivel de gestión y planificación, el agilismo minimalista no es ninguna sorpresa. El punto es que si no implementas todas estas buenas prácticas, no eres ágil. Simplemente no vas a poder reaccionar a los cambios en tu proyecto ni mejorar la forma de trabajo. Casi todas estas prácticas las realizan todas las empresas serias que conozco, excepto la reunión diaria, las historias de usuario y la implementación por orden de valor. Si no practicas la reunión diaria tendrás problemas ocultos y enquistados al menos hasta la próxima reunión de seguimiento, que será dentro de ¿entre 2 y 4 semanas? ¿No es muy ágil cierto? Hasta aquí casi todos estareis de acuerdo, pero todo esto no es suficiente para reaccionar ante problemas y mejorar en un proyecto software. Necesitamos tener en cuenta otro aspecto, la ingeniería.

Desde el punto de vista de mejora continua de la ingeniería y desarrollo:

  • Programación por parejas. Es otro ciclo de feedback, esta vez sobre el diseño y la calidad de código. Lo podemos considerar una revisión de código continua o una QA continua. El feedback proporcionado por un compañero a la persona que está tecleando es rapidísimo, del orden de segundos.
  • TDD. Nos permite saber si nuestro código funciona, o si la modificación que hemos hecho rompe algo. Feedback del orden de minutos.
  • Refactor frecuentemente. Dentro de un ciclo de TDD para garantizar que se hace a menudo, y guiado por la programación por parejas, nos permite mejorar de forma continua la calidad de nuestro código.
  • Integración Continua. Feedback a nivel de horas, que nos permite detectar problemas de integración.

Bien, con esto quiero decir que el agilismo minimalista necesita de mejora continua no sólo a nivel de gestión, sino a nivel de ingeniería, con lo cual necesitas hacer TDD, refactor, pair programming e integración continua, como mínimo, para ser ágil. Si no haces integración continua no detectas los problemas de integración hasta que no haces un pase, ¿es eso ser ágil? Si no haces TDD no detectas si has roto funcionalidad al introducir un cambio hasta que no lo prueba el equipo de QA, ¿es eso ser ágil? Además si no haces TDD no puedes hacer integración continua, sino como mucho, compilación continua. Si no haces pair programming no detectas errores de programación o fallos de diseño hasta dios sabe cuando, ¿es eso ser ágil? El refactor frecuente te permite arreglar tu código, sin necesidad de esperar al super arquitecto/guru. Lo importante es que todas estas prácticas se realimentan y se producen sinergias positivas entre ellas. Cada una cubre cosas que la otra no. Si quitas una sola el edificio se empieza a derrumbar. Por ejemplo si quitas el pair programming puedes introducir fake TDD o simplemente saltarte el refactor. Si quitas el TDD te cargas la integración continua. Las necesitas todas, no son opcionales, son obligatorias.

Ya contamos en la CAS2010 nuestra experiencia de implantación del agilismo. Hubo un momento que teníamos sprints, retrospectivas, etc. Pero no teníamos TDD ni pair programming ni se hacía refactor, y mucho menos integración continua. Eso no funcionaba. Asi que lo siento, a aquellos que piensen que puedes hacer agilismo con un equipo que no controle estas técnicas y no esté dispuesto a hacerlas, os aviso: no va a funcionar, no basta con el sprint planning y el daily scrum, no vas a ser ágil, necesitas prácticas ágiles de ingeniería y gente capaz de llevarlas a cabo.

Afortunadamente parece que hay gente por ahí de acuerdo conmigo, sino leed lo mismo que digo pero explicado de otra manera en este blog de Luis Artola. SCRUM y KANBAN sólo se ocupan del nivel de gestión y planificación de proyecto, pero no de la ingeniería. Para que tu proyecto sea exitoso, necesitas también buenas prácticas de ingeniería y trabajadores capaces de llevarlas a cabo. Muchos piensan que son ágiles porque aplican prácticas ágiles a nivel de planificación y gestión, pero se olvidan de la ingeniería. Esta es una maldición eterna en el mundo de los proyectos software, parece que por poner una capa de gestión se arreglan todos los problemas, pero nadie presta atención a lo más básico, la ingeniería y la capacidad profesional de los “pikachus”.

En resumen, lo mínimo que puedes hacer para considerarte ágil (agilismo minimalista) es: pair programming, TDD+Refactor, integración continua, reuniones diarias, reuniones periódicas de seguimiento (internas y externas para el cliente), retrospectivas de proyecto y planificación y estimación basada en historias de usuario. Si te falta, aunque sea una sola de estas prácticas, no eres ágil.

¿Y las demás buenas prácticas? Bueno, para mi no forman parte del agilismo minimalista, las considero muy dependientes del entorno en el que se mueva cada uno. Aquí es, donde creo yo, que se debe aplicar eso de probar a usar una buena práctica, experimentar y ver si va con tu proyecto o no. Lo importante es que con el agilismo minimalista irás descubriendo cuales son útiles y cuales no. Por ejemplo, a nosotros los sprints no nos han funcionado bien, por eso estamos pensando en pasar a kanban, pero conservando los principios del agilismo minimalista. Quizás esto de agilismo minimalista+kanban sea algo parecido al scrumban que he escuchado por ahí. En cualquier caso el resto de las prácticas debéis experimentar con ellas antes de adoptarlas en serio o no.

Corolario: si ves que necesitas métricas estáticas de calidad de código, es una señal de que no eres ágil, algo falla (pesaito soy). Saludos y ahora me voy a dormir, que ya me vale.

Read Full Post »


Hola a todos, de vuelta de la AgileSpain2010, y con un poco de “resaca” de la conferencia, me toca defender un par de frases que solté en nuestra sesión. Lo que ocurrió realmente fue lo siguiente:

Creando polémica en la CAS2010

Fanático del TDD en la CAS2010

Bien, pues lo solté y me quedé tan fresco, de hecho no me pareció que fuera una frase polémica, pero empecé a ver caras raras y a la salida de la sesión vi por twitter que mi frase había causado cierta extrañeza. Empezaré aclarando mi frase: “Si haces TDD bien no necesitas análisis de calidad de código estático”. Ojo, hablo de hacer TDD bien, no de hacer TDD a medias. Existe un malentendido respecto al objetivo del TDD, si bien uno de ellos es lograr un conjunto de pruebas automatizadas con alta cobertura de código, éste no es el único, de hecho es sólo la mitad de la historia. El otro objetivo, igualmente importante, es conseguir un código de calidad de forma incremental y/o evolutiva. De hecho algunos autores hablan de calidad o diseño emergente, pero yo prefiero no fliparme tanto de momento. Si tenéis el libro de Carlos Blé sobre TDD, veréis que se llama “Diseño Ágil con TDD”, no programación con TDD o pruebas con TDD o QA con TDD, sino diseño. Éste es el entendimiento general que todos los autores tienen sobre este tema: TDD lleva a pruebas automáticas de alta cobertura y a alta calidad de código, si lo haces bien, claro.

Llegado a este punto conviene explicaros mi percepción de los niveles de adopción del TDD:

  • Fake TDD. En este nivel de adopción el TDD no se practica, sino que se simula practicar. En el fake TDD los tests no representan realmente la funcionalidad de las historias de usuario o de la interfaz del componente que queremos probar. No se hace un esfuerzo serio por entender la funcionalidad del componente bajo pruebas y se escriben tests con poco contenido, contenido incorrecto o simplemente tests de relleno sin contenido. Ésto puede ser por desconocimiento de la técnica, con lo que habremos de dar más formación y hacer talleres. También puede ser por presión, que tiende a romper la disciplina del programador, y por miedo a no estar en fechas, se ignora el TDD. La tercera razón para hacer fake TDD la veremos en el siguiente post.
  • Naive TDD. Simplemente consiste en no realizar la fase de refactorización durante el ciclo de TDD. Las causas suelen ser de nuevo inexperiencia o prisas. Normalmente si no se entiende TDD como una metodología de diseño nos encontraremos en este caso. Otro tipo de Naive TDD se produce cuando se escribe primero código de implementación y después el test.
  • TDD. Adopción completa, haces test con funcionalidad correcta pero no consideras el ciclo terminado hasta que no has refactorizado. Para que un ciclo de TDD se considere completo y puedas hacer commit, debe existir un test, con los contenidos adecuados, que al ejecutarse pase, y el código que implementa la funcionalidad bajo prueba sea limpio. Si el código no es limpio debemos refactorizar.

¿Qué es pues código limpio? Pues es código que tiene unos niveles de calidad razonables, pero, ¿qué es la calidad del código? Difícil pregunta. Para ello las distintas organizaciones y empresas definen un modelo de calidad, que consiste en un conjunto de métricas de código que se van a tomar y que resultados mínimos son exigibles para esas métricas. Las métricas se clasifican en dos tipos: estáticas y dinámicas.

La métricas dinámicas miden propiedades de tiempo de ejecución del sistema. Ejemplos típicos son corrección, rendimiento y escalabilidad, seguridad y usabilidad:

  • La corrección la conseguimos con el propio TDD (hasta donde es posible dado que los requisitos son cambiantes y difusos).
  • El rendimiento y escalabilidad se consiguen con pruebas de stress, algo que está aparte del TDD, hasta donde yo sé.
  • La seguridad del mismo modo está en un mundo aparte, y que yo sepa no se pueden hacer pruebas automatizadas satisfactorias para esto, salvo quizás los ataques más típicos.
  • La usabilidad tiene que ver con la facilidad de manejo de la aplicación y su atractivo para el usuario, definitivamente esto no se puede probar de forma automática, para ello podemos usar pruebas de aceptación tradicionales.

Por otro lado las métricas estáticas miden propiedades del código que se pueden detectar en tiempo de compilación. Veamos:

  • Nomenclatura. Si tu código se ajusta o no a determinada nomenclatura. Bueno, que deciros, no considero esto importante para un desarrollo ágil, es mucho más importante la legibilidad y la documentación. Puedo entender que en un lenguaje con tipado dinámico esto pueda ayudar, al fin y al cabo no hay compilador que te diga si una variable es un string o un integer. Sin embargo si vas a usar un lenguaje de tipado dinámico (ruby, javascript, smalltalk, etc) es mejor que no pienses en java, sino que saques partido a las características de dicho lenguaje, que normalmente no está pensado para que una variable sea siempre un string, es simplemente otra filosofía de diseño.
  • Nivel de documentación. Bueno, esto sí es interesante, me habéis pillado :-) Sin embargo no considero que esto se pueda medir de forma totalmente automática. La dificultad radica en que es muy difícil automatizar la decisión de si un método o clase debe estar documentada. Desde el punto de vista del agilismo debemos documentar sólo aquello que merezca la pena, no todo, y por supuesto tampoco vale no documentar nada. Detectar si documentar un artefacto de código “vale la pena” o “o aporta valor” de forma automática es difícil, ¿no creéis?
  • Legibilidad. Lo más importante, tu código debe ser legible por tus compañeros, si no, no podrán mantenerlo. Esto tampoco se puede detectar automáticamente.
  • Estilo y formato de código. Esto realmente es un aspecto de la legibilidad.
  • Tamaño del sistema. No se muy bien para que se quiere medir esto, y además, ¿en qué lo medimos? ¿Lineas de código?¿Puntos función? Sin comentarios. Tal vez lo que queremos medir realmente es la cohesión.
  • Alta cohesión y bajo acoplamiento (principio de una única responsabilidad). Realmente estas son muy interesantes de medir y a mi juicio sí que son necesarias. Los artefactos de código que tengan muchas interdependencias entre ellos deben agruparse en artefactos de nivel superior. Por ejemplo, un montón de métodos que son muy interdependientes podrían agruparse en la misma clase. Y viceversa, si un artefacto esta compuesto de muchos subartefactos que apenas interaccionan entre si, podemos dividir ese artefacto en varios más pequeño. El ejemplo típico es la clase “monstruo” con todos los métodos del sistema dentro de ella, ay, cuantas de éstas habré visto a lo largo de mi carrera. Estas propiedad del sistema sí que se pueden medir automáticamente, al menos en los lenguajes de tipado fuerte. Si se pueden medir automáticamente en los lenguajes de tipado dinámico tengo mis dudas, pero me callo por no ser experto.
  • Duplicación de código. Esto se puede medir fácilmente con una herramienta. La existencia de código duplicado es un signo de mal diseño y baja calidad, excepto en el caso de extrema necesidad de legibilidad.
  • Malas prácticas. ¿Acaso el analizador de código estático es inteligente? No, es tonto, es un robot que se limita a pasar patrones sobre el código. Si no tiene inteligencia, ¿cómo va a saber si una práctica es mala? ¿Acaso entiende el código? Sólo va a detectar las malas prácticas más sencillas. No me lo creo a pesar de lo que me digan los vendedores de herramientas sofisticadas (y caras).

Existen muchísimas más métricas estáticas, algunas de alta complejidad. Yo personalmente no las entiendo, ni veo cual es su sentido. Todavía no conozco a nadie que me las haya podido explicar, así que de momento me quedo con las que os he contado anteriormente. En cualquier caso, ¿qué utilidad tiene medir una cosa sin entender lo que estás midiendo? Si alguien me sabe explicar alguna métrica arcana, que sea esencial para medir la calidad del software, y no haya mencionado, que me lo comente.

En el modelo de calidad que yo uso, y que me gusta pensar que es un modelo de calidad ágil, considero que el código es limpio si cumple en la medida de lo posible los siguientes criterios de calidad: corrección, legibilidad, sin código duplicado, bajo acoplamiento y alta cohesión. Si os fijáis, excepto la legibilidad, el resto de los criterios te los incorpora de forma natural el ciclo de TDD que incluye la refactorización. La legibilidad es algo que no se puede comprobar automáticamente. O sea que si hacemos TDD bien, y encima lo reforzamos con otras buenas prácticas, como la programación por parejas o la revisión de código, no necesitamos para nada complicar vuestro build con análisis de código estático. Una aclaración, yo considero que el código es limpio si todo el código pasa el modelo de calidad anteriormente mencionado. Esto implica no sólo el código de implementación, sino el código de test. La tarea de refactorización incluye también al código de test no sólo el código bajo pruebas. No es válido tener código duplicado en el código de test, y este debe ser legible, y poseer una buena cohesión. De hecho las pruebas unitarias deben ser independientes unas de otras, lo que implica un acoplamiento entre ellas muy bajo. Un error típico es no refactorizar el código de test, sólo el de implementación.

Muchos estareis pensando que todo esto es muy bonito, pero lo que queréis realmente es controlar que las cosas se hacen bien. Así que muchos me diréis, ¿cómo controlo que el código está bien hecho?¿Cómo se que se está haciendo TDD bien y no me están engañando? Muchos de los expertos en agilismo os dirán que el simple hecho de que hagáis esta pregunta significa que no sois ágiles. Si necesitáis que una herramienta os diga si el equipo está programando según las buenas prácticas es que no estáis al lado del equipo, que no practicáis go&see y que estais encerrados en vuestros despachos sin interactuar realmente con la realidad, es decir, con vuestro equipo.

Ciertamente lo comentado en el anterior párrafo puede ser cierto en las circunstancias típicas de un proyecto ágil. En otras circunstancias, como en el caso de trabajar con equipos distribuidos, es lícito que tengáis dudas sobre si vuestro equipo hace el TDD como debiera, ya que al fin y al cabo, no podéis estar en la misma sala con todos los equipos a la vez. Así pues, si tienes equipos distribuidos, ¿cómo sabes que se hace bien el TDD? La respuesta está en el nivel de cobertura y en aplicar un poco de psicología:

  • Si la cobertura es baja, entonces realmente no se está haciendo TDD bueno. Puede ser porque se está haciendo Fake TDD, y por lo tanto los tests al no ser suficientemente completos no ejerciten bien el código. Puede ser también que tengamos Naive TDD, y se escriba código de implementación antes que el test, con lo que puede quedar zonas del código de implementación sin probar. La falta de refactorización del naive TDD también puede llevar a baja cobertura, después lo comento. También puede ocurrir que simplemente no se ha hecho TDD en todos los casos, y se ha escrito código de implementación sin ningún tipo de test.
  • La cobertura es alta (mayor del 70%). En este caso existe una alta probabilidad de que el código tenga una buena calidad y se haya hecho TDD del bueno con refactorización. La razón de esto es sutil. Lógicamente si hacemos TDD bien vamos a tener una cobertura alta. ¿Podemos llegar a tener cobertura alta haciendo Fake TDD? Claramente no, a menos que el programador dedique sus esfuerzos a sabotear el proyecto y haga casos de tests con alta cobertura, que pasen pero que no prueben nada. Ciertamente esto es bastante bizarro. Sobre la gente que se dedica a hacer esto hablaré en el siguiente post ¿Pero y el Naive TDD? Al fin y al cabo sí hacen tests, podríamos tener alta cobertura a pesar de no refactorizar, ¿no? La verdad es que ciertamente es posible pero es difícil. Al no refactorizar duplicas código, y el diseño de tus clases hace que se vayan volviendo cada vez más difícil de testear, ya que no guardan el principio de alta cohesión, bajo acoplamiento, etc. Por un lado duplicar código hace que tengas que duplicar también los fragmentos de test que ejercitas ese código. Esto se va haciendo pesado y se tiende naturalmente a no hacerse con lo cual la cobertura baja. También la duplicación invita a que los tests fallen, ya que un cambio en una historia de usuario o un bug, implica cambiar código en muchas zonas de la aplicación, con lo que harán que el test que prueba ese bug falle. Finalmente, al no refactorizar, las clases van perdiendo calidad, y va siendo cada vez más difícil hacer pruebas unitarias, ya que estas se van acoplando cada vez más con su entorno, y no presentan una buena encapsulación y cohesión. Como vemos el no refactorizar al principio no tiene mucha importancia, pero conforme pasa el tiempo se hace más difícil añadir tests, con lo cual los tests se hacen más laxos y la cobertura baja.

Yo recomiendo encarecidamente mezclar TDD con dos prácticas más: programación por parejas y revisiones de código, especialmente de los tests. Ambas contribuyen a que los desarrolladores cuiden su prestigio y no pierdan la disciplina, con lo que baja la probabilidad de que rompan el ciclo de TDD. La programación por parejas tiene la ventaja adicional de enseñar la técnica a los más novatos. Os puedo contar que en el último proyecto estaba presionado de tiempo y rompí la disciplina del TDD para una funcionalidad que era “trivial”. Como no estaba haciendo programación por parejas pude hacer esta trampa. ¿El resultado? ¡ Zas, en to la boca ! Efectivamente sufrí una concentración de bugs en ese código “trivial”, necesité una jornada de 20 horas de programación ininterrumpida para solventar el desaguisado. Cuando llegué a casa mi mujer casi me mata de un sartenazo pensando que era un vulgar ladrón. Después estuve una semana KO por el sobreesfuerzo. Nunca más.

Si aun veis que queréis un análisis estático de código podéis usarlo como método didáctico. Conforme vais haciendo TDD, ireis viendo como vuestra cobertura aumenta y también los resultados de las métricas de calidad. Esto es recomendable para equipos que empiezan con el TDD y todo esto de la refactorización. Cuando se trata de un equipo que sabe hacer TDD y refactor, realmente las métricas os sobran, así que podéis aligerar vuestro sistema de integración continua y ahorraros un dinero en licencias desactivando tales métricas.

Por supuesto existe una razón no ágil para esto de las métricas: el informe de colores para impresionar a la dirección y a los clientes. Si necesitáis un informe lujurioso para justificar el avance de vuestro proyecto estais en una situación mala. Yo justifico el avance de mis proyectos con demos, enseñando software que funciona y que cada vez tiene más funcionalidad.

En el siguiente post os cuento la otra pequeña polémica que tuvimos.

Read Full Post »

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

Únete a otros 42 seguidores