¿Estoy haciendo bien TDD? Una herramienta para facilitar su aprendizaje

Matias Dinota
Eryx
Published in
6 min readOct 8, 2020

Si estás en el mundo del desarrollo de software seguro, en algún momento, escuchaste hablar de Test Driven Development o TDD. Tal vez escuchaste que surgió por el año 2000 impulsada sobre todo por Kent Beck (el mismo que años antes publicaba sobre Extreme Programming). A partir de entonces fue ganando importancia y seguidores sobre todo en el ambiente de las metodologías ágiles. También ganó algún que otro detractor. Pero ¿Qué es TDD?

TDD es una forma de hacer software. Este se construye en ciclos cortos siguiendo estos pasos:

  1. Se escribe un test pequeño que falle. El más simple que se nos ocurra en general
  2. Se hace lo mínimo posible para hacerlo pasar
  3. Se mejora el código escrito, en general para arreglar las cosas horribles que hicimos en el paso 2
  4. Volvemos al paso 1

Simple, ¿no?

Esta forma de encarar el desarrollo de software tiene muchas ventajas. Permite aprender sobre el problema a través de ejemplos concretos (cada test), se obtiene una base sólida de tests que nos permiten modificar nuestro código con confianza, se hace foco en la mejora constante (en cada ciclo tenemos que pensar qué podríamos hacer mejor), entre varias otras.

Sin embargo, a pesar de que suene tan simple, a la hora de llevarlo a la práctica las cosas se pueden complicar.

Hace unos años…

En mi experiencia, cuando empecé con TDD, me costaba pensar tests chicos y que pudiera hacerlos pasar rápido. Escribía un test que abarcaba mucho “funcionamiento” y después estaba horas tratando de hacer que pase.

También me pasaba que, una vez que lograba hacerlo pasar, estaba tan concentrado en el código que resolvía el problema que capaz le agregaba alguna otra funcionalidad (¿una sola?) e internamente me prometía testearla después. Seguramente ya habrán adivinado que eso nunca terminaba testeado.

Finalmente, me daba cuenta que había estado codeando tres horas y tenía dos tests así que, para calmar culpas, escribía algunos tests más para cubrir lo esencial. Escribía un par de tests para casos simples — pasaban sin problemas — . Cuando quería escribir algo más complejo tenía que cambiar mi código, que ya estaba funcionando, para poder hacerlo “testeable”. Era entonces cuando me auto-convencía de que mi código funcionaba y que no necesitaba más tests. Commit, push y a seguir con otra tarea, feliz por haber hecho TDD (a medias, si lo vemos con muy buenos ojos).

Aunque me cueste admitirlo, tardé un tiempo en darme cuenta que eso no me daba casi ninguna de las ventajas que nos da TDD. Había partes del código que no estaban testeadas y, por lo tanto, no tenía una base sólida de tests. Tenía miedo de cambiar el código que ya estaba funcionando, aún para escribir un test sobre eso. Y, por último, los tests que había agregado al final, en general, eran inútiles y no aportaban demasiado.

TDDGuru

Con los años, fui mejorando en la forma de hacer TDD. Principalmente por mi trabajo en Eryx pero también tuve la suerte de llevarlo al ámbito universitario.

Mi tesis de Licenciatura está orientada a facilitar el aprendizaje de TDD y resolver algunos de los problemas que comentamos arriba. Junto con mi director, Hernán Wilkinson, creamos una herramienta a la que llamamos TDDGuru. Esta analiza el historial de cambios del programador y determina si se respetaron las prácticas de TDD y, en caso de encontrar un error, se indica qué se debería haber hecho.

Está desarrollada en Cuis, un dialecto de Smalltalk argentino, y su objetivo es asistir en el aprendizaje de TDD de manera no intrusiva. El programador realiza su trabajo normalmente y al finalizar (total o parcialmente) puede analizarlo con TDDGuru.

¿Cómo funciona?

Creo que la mejor forma de mostrarlo es con un ejemplo concreto. Vamos a hacer bien TDD al principio y después vamos equivocarnos a propósito para ver el resultado.

Supongamos que queremos programar una cuenta bancaria en la que se puede depositar/extraer plata y consultar su saldo.

Arranquemos bien, escribiendo un test fácil: las nuevas cuentas deberían tener saldo 0.

testNewBankAccountStartsWithZeroBalance  | bankAccount |  bankAccount := BankAccount new.  self assert: bankAccount balance equals: 0.

Corremos el test y falla porque el objeto BankAccount no responde el mensaje balance. Tiene sentido. Lo implementamos de la manera más fácil que se me ocurre:

balance  ^ 0

Corremos nuevamente el test y, por supuesto, pasa. Sin embargo, nos queda esa constante 0 en el método balance que hace un poco de ruido. Creo que podemos mejorarlo usando una variable de instancia (o atributo). Definimos el método initialize (se llama cada vez que se crea una instancia) y reescribimos el método balance de la siguiente forma:

initialize  balance := 0balance  ^ balance

Corremos los tests y vuelven a pasar. Cerramos el primer ciclo de TDD de manera impecable. Veamos qué nos muestra TDDGuru.

Se muestran las acciones del programador y se remarcan en verde/rojo si son correctas/incorrectas según TDD

Hasta ahora, todos los cambios que hicimos figuran en verde. ¡Seguimos adelante!

Escribamos un segundo test: si deposito 20 en una nueva cuenta el saldo debería ser 20.

testDepositAmountInANewAccount  | bankAccount |  bankAccount := BankAccount new.  bankAccount deposit: 20.  self assert: bankAccount balance equals: 20.

Corremos el test. Falla porque aún no definimos el método deposit:anAmount. La implementación parece bastante fácil.

deposit: anAmount  balance := balance + anAmount

Corremos los dos tests que tenemos hasta ahora y los dos pasan. Hasta acá venimos muy bien.

Complicando un poco las cosas…

Vamos a alejarnos un poco de las reglas que establece TDD para mostrar las ayudas que nos da la herramienta. La implementación del método withdraw: anAmount debería ser muy similar a la de deposit: anAmount así que me siento confiado para definirlo sin escribir ningún test.

withdraw: anAmount  balance := balance — anAmount

A esta altura, si analizamos con TDDGuru obtenemos lo siguiente:

Lo veíamos venir. Definimos el método withdraw: anAmount y nunca lo usamos (ni en el código existente ni en un test) así que claramente estamos agregando comportamiento no testeado.

Supongamos también que nos dimos cuenta del error y decidimos testear el funcionamiento del método que agregamos.

testWithdrawFromBankAccountWithEnoughBalance  | bankAccount |  bankAccount := BankAccount new.  bankAccount deposit: 30.  bankAccount withdraw: 10.  self assert: bankAccount balance equals: 20.

Lo corremos pero, a diferencia de lo que propone TDD, el test pasa. Esto lo podemos ver también en TDDGuru.

Recapitulando

A esta altura creo que ya nos damos una idea de cómo funciona la herramienta.

Como dijimos antes, está pensada para quienes se están iniciando en TDD. Para asistir en el aprendizaje de la práctica y motivar a que puedan sacarle el jugo (a diferencia de mis primeros acercamientos a TDD).

Si alguno/a está interesado/a en conocer más sobre el proyecto puede ver el código acá.

Por último, les dejo una cita que encontré en el tiempo que estuve trabajando en este proyecto que creo que resume bastante bien el espíritu “test-infected” de TDD.

I am good at fooling myself into believing that what I wrote is what I meant. I am also good at fooling myself into believing that what I meant is what I should have meant. So I don’t trust anything I write until I have tests for it.

K. Beck

--

--