20 de noviembre de 2014

Probamos el béisbol y no funcionó

por Ron Jeffries
¿Una alegoría?
¿Sarcasmo?
¿Plagio humorístico?

Usted decida.
Los defensores fanáticos del béisbol nos dicen que es un juego muy emocionante, divertido de jugar y divertido de ver. Ellos son claramente estúpidos o malvados, o ambos, porque nosotros probamos el béisbol y no funcionó.

En primer lugar, los requisitos para el juego son estúpidos: no escalan. Dicen que se necesitan al menos nueve jugadores por lado. Eso es estúpidamente ineficiente. El número mínimo de jugadores es claramente cuatro: tres hombres envasados y uno bateando. Esa es la forma en que nosotros jugamos: cuatro personas por lado.

Con sólo cuatro jugadores, no necesitamos todas esas bases (otra ineficiencia evidente y la falla de escala), así que sólo utilizamos una base: el home. Cuando un bateador hace un hit, sólo se corre alrededor del diamante (pero véase más adelante), regresando a la base de "home". Al principio hacíamos al corredor recorrer los noventa pies por lado marcados por los proponentes del béisbol, pero por alguna razón los chicos del béisbol no se dieron cuenta que, en la mayoría de los casos, ¡360 ​​pies son demasiado para que el corredor los corra en un solo hit!. Nadie pudo completar toda la vuelta sin ser puesto out. Encontramos que un círculo con un diámetro de 28,64 pies es ideal. (El círculo, con su relación óptima de área contra circunferencia, es mucho más eficiente que el cuadrado propuesto por los promotores del béisbol.)

El equipo "requerido" por estos chicos del béisbol es ridículo, anacrónico, exigente y cruel. En primer lugar, se supone que la "bola" esté cubierta con piel de caballo. Esto es a la vez cruel con los caballos, y discriminatorio en contra de los proveedores tradicionales de forros para bolas: los cerdos y las vacas. En vista de esto, hemos utilizado otras bolas. Tratamos una pelota de cordel sin cubierta, pero su tamaño disminuye continuamente, haciendo el juego cada vez más difícil, especialmente en las últimas entradas. Las pelotas de golf viajan demasiado lejos y eran difíciles de atrapar. Uno de nuestros equipos utilizó las cabezas de muñecas que habían tomado de sus hermanas, pero esto condujo a dificultades en el hogar. El cómo los proponentes del béisbol pueden impulsar un juego que lleva a este tipo de problemas, va más allá de nuestra comprensión.

Finalmente nos decidimos por los calcetines de lana, rellenos con papel de desecho y anudados. Estos eran fáciles de atrapar, no iban demasiado lejos, y el resto colgante del calcetín los hace fáciles lanzar.
Los llamados "bats"1 son también una mala idea. En primer lugar no son nada parecido a esos peludos caza-insectos del cielo nocturno. La confusión causada por ese nombre ha retrasado más de un juego. Además, los "bats de béisbol" (tuvimos usar el nombre totalmente calificado) son caros de adquirir y difíciles de fabricar. Tratamos dejar que cada hombre al bat usara cualquier cosa que quisiera, pero esto retrasó el juego y dificultó la tarea de llevar estadísticas (una parte clave del béisbol, de acuerdo con sus proponentes). Nos ajustamos a usar de palos de los alrededores del campo. Las ramas de pino funcionan mejor, ya que son fáciles de romper las ramas de los árboles, y como las manos se ponen pegajosas por la resina, se hace más fácil atrapar el calcetín, cuando se está en el campo.

Y hablando del campo, otro problema que los defensores del béisbol no han resuelto adecuadamente es que con un pitcher, un catcher, y sólo dos fielders, la bola va a dar con demasiada frecuencia lejos de los jugadores, lo que causa correr en exceso y también que los juegos tomen mucho tiempo y tengan puntajes demasiado altos. Finalmente nos dimos cuenta - por qué los chicos del béisbol originales no lo hicieron, no lo podemos imaginar - que el uso de uno de los jugadores como el catcher es terriblemente ineficiente. En cambio, si el "palero" --el término "batter" es a la vez ambiguo e inadecuado en vista de los cambios en el equipo que encontramos necesarios para poder jugar en absoluto-- no conecta, simplemente recoge el calcetín él mismo y lo arroja de nuevo al lanzador. Esto nos permite disponer de los cuatro jugadores defensivos en el campo, lo cual es un uso mucho más eficiente de los recursos, una prueba más de que el béisbol no escala.

Sin embargo, lo que finalmente nos hizo abandonar toda esa idea del "béisbol", es lo siguiente: a pesar de todas las mejoras que hicimos, ¡el juego no es divertido en absoluto!

Probamos el béisbol y no funcionó.


Artículo original disponible en http://xprogramming.com/articles/jatbaseball.

Notas

1. Se conservan los términos en inglés ya que en caso contrario, las bromas en doble sentido (pun) se pierden con la traducción.

6 de marzo de 2013

String Calculator revisada

Bueno, después de practicar esta kata unas cuantas veces, logré bajar mi tiempo promedio a 12 minutos. Esta vez, ¡cumpliendo con todos los requerimientos del kata!

A continuación anexo el código actualizado:

Pruebas:

#!/usr/bin/env python

import unittest
#from strcalc_re import add
from strcalc import add


class SimpleTest(unittest.TestCase):

  def testEmptyString(self):
    self.assertEquals(0, add(''))

  def testSingleNumberString(self):
    self.assertEquals(2, add('2'))
    self.assertEquals(2, add('\n2'))
    self.assertEquals(2, add('2\n'))

  def testTwoNumberString(self):
    self.assertEquals(7, add('2\n5'))
    self.assertEquals(7, add('2, 5'))

  def testThreeNumberString(self):
    self.assertEquals(15, add('2,5,8'))
    self.assertEquals(15, add('2,5\n8'))


class UserDefinedSeparators(unittest.TestCase):

  def testEmptyString(self):
    self.assertEquals(0, add('//|\n'))

  def testSingleNumberString(self):
    self.assertEquals(2, add('//;\n2'))

  def testTwoNumberString(self):
    self.assertEquals(7, add('///\n2/5'))

  def testThreeNumberString(self):
    self.assertEquals(15, add('//#\n2#5#8'))
    self.assertEquals(15, add('''//!\n2!5\n8'''))


class NegativesNotAllowed(unittest.TestCase):

  def runTest(self):
    with self.assertRaises(ValueError):
      add('2,-1')
    with self.assertRaises(ValueError):
      add('//&\n2&-1')


class IgnoreGreaterThan1000(unittest.TestCase):

  def runTest(self):
    self.assertEquals(1, add('1\n1001'))
    self.assertEquals(8, add('//$\n1\n1002$3$4$1005'))


class ArbitraryLengthSeparators(unittest.TestCase):

  def runTest(self):
    self.assertEquals(8, add('//[$$$$]\n1\n1002$$$$3$$$$4\n0'))
    self.assertEquals(6, add('//[***]\n1***3***2\n**1'))


class MultipleSeparators(unittest.TestCase):

  def runTest(self):
    self.assertEquals(10, add('//[$][#][%]\n1$2#3%4'))
    self.assertEquals(10, add("//[a][lil][delim]\n1a2lil3delim4"))


unittest.TestSuite.addNestedSuites = lambda self, *classes: \
  self.addTests(map(unittest.makeSuite, classes))

unittest.TestSuite.addAllTests = lambda self, *classes: \
  self.addTests(cls() for cls in classes)


def suite():
  result = unittest.TestSuite()
  result.addNestedSuites(SimpleTest, UserDefinedSeparators)
  result.addAllTests(NegativesNotAllowed, IgnoreGreaterThan1000,
                     ArbitraryLengthSeparators, MultipleSeparators)
  return result


if __name__ == '__main__':
  runner = unittest.TextTestRunner(verbosity=2)
  runner.run(suite())

Existen algunas diferencias con respecto a la última versión que publiqué:

  • A diferencia del post anterior, esta ocasión he organizado las pruebas según el requerimiento, a fin de hacerlas más comprensibles.
  • La clase MultipleSeparators incluye el caso de prueba para los requerimientos 8 y 9.
  • Las lineas 4 y 5 indican que estas pruebas pueden ser usadas con dos implementaciones del código de producción: strcalc y strcalc_re.

Código: Implementación con expresiones regulares

#!/usr/bin/env python

import re


def parse_separators(line):
  if line.startswith('//[') and line.endswith(']'):
    result = line[3:-1].split('][')
  else:
    result = line[2:3]
  return '|'.join(re.escape(r) for r in result)


def config(s):
  separators = ','
  lines = s.split()
  if lines and lines[0].startswith('//'):
    separators = parse_separators(lines[0])
    lines = lines[1:]
  return (lines, separators)


def to_num(s):
  try:
    return int(s)
  except ValueError:
    return 0


def to_nums(lines, sep):
  pattern = re.compile(sep)
  return [to_num(part) for line in lines for part in
          pattern.split(line)]


def negatives_in(nums):
  return [n for n in nums if n < 0]


def add(s):
  (lines, sep) = config(s)
  nums = to_nums(lines, sep)
  negatives = negatives_in(nums)
  if negatives:
    raise ValueError('No se permiten negativos: %r' % negatives)
  return sum(n for n in nums if n <= 1000)

La forma más sencilla que encontré de soportar múltiples separadores fue utilizar re.split (linea 33). La complicación, claro, es que los delimitadores soportados por la implementación deberían ser arbitrarios. Sin embargo, re interpreta ciertos caracteres de forma especial, así que es necesario evitar que re haga de las suyas. ¿La solución? Escapar los delimitadores antes de llamar a re.split (linea 11).

Código: Implementación sin expresiones regulares

Por supuesto, quienes me conozcan sabrán que no me iba a contentar con la solución de la librería de python sin haber por lo menos implementado esa solución por mi mismo una vez. El resultado es algo así:

#!/usr/bin/env python

import re

def parse_separators(line):
  if line.startswith('//[') and line.endswith(']'):
    return line[3:-1].split('][')
  return [line[2:3]]


def config(s):
  separators = [',']
  lines = s.split()
  if lines and lines[0].startswith('//'):
    separators = parse_separators(lines[0])
    lines = lines[1:]
  return (lines, separators)


def to_num(s):
  try:
    return int(s)
  except ValueError:
    return 0


def to_nums(lines, sep):
  for s in sep:
    lines = sum((line.split(s) for line in lines), [])
  return map(to_num, lines)


def negatives_in(nums):
  return [n for n in nums if n < 0]


def add(s):
  (lines, sep) = config(s)
  nums = to_nums(lines, sep)
  negatives = negatives_in(nums)
  if negatives:
    raise ValueError('No se permiten negativos: %r' % negatives)
  return sum(n for n in nums if n <= 1000)

Las lineas resaltadas indican las ediciones para pasar de una versión a la otra.

La parte más dificil de esta versión fue encontrar una manera eficiente y legible de expandir una lista anidada en una lista simple con todos los elementos de sus sub-listas. Despues de probar varias soluciones llegué a una comprensión un tanto monstruosa:

lines = [part for parts in [line.split(s) for line in lines] for part in parts]

¡Ugh!, ¡Me lloran los ojos de solo verla!

La solución es simplemente que sum aplica el operador + a todos los elementos de la secuencia que recibe como argumento, sustituyendo in situ el resultado y las listas, claro está, implementan dicho operador (linea 29). Esta aplicación puede parecer un hack, por lo que es posible encapsularla en una función con un nombre ilustrativo como:

def to_nums(lines, sep):
    flatten = lambda nested_list: sum(nested_list, [])
    for s in sep:
        lines = flatten(line.split(s) for line in lines)
    return map(to_num, lines)

Sin embargo, me pregunto ¿qué tanto realmente gana el código en legibilidad con dicho cambio?

Saludos

6 de febrero de 2013

String Calculator en Python

Para complementar la entrada del joven +Alcides Flores Pineda, aquí anexo mi versión de la Kata en Python:

Pruebas:

#!/usr/bin/env python
import unittest
from strcalc import calc

def expected(exception):
    def argcatcher(f):
        def wrapper(self, *args):
            self.assertRaises(exception, lambda: f(self, *args))
        return wrapper
    return argcatcher


class StringCalcTest(unittest.TestCase):
    def testEmptyString(self):
        self.assertEquals(0, add(""))

    def testOneNumberString(self):
        self.assertEquals(2, add("2"))

    def testTwoNumberString(self):
        self.assertEquals(7, add("2,5"))

    def testThreeNumberString(self):
        self.assertEquals(15, add("2,5,8"))

    def testThreeNumberStringNewLine(self):
        self.assertEquals(15, add("2,5\n8"))

    def testEmptyStringWithUserSeparator(self):
        self.assertEquals(0, add("//;\n"))

    def testOneNumberStringWithUserSeparator(self):
        self.assertEquals(2, add("//;\n2"))

    def testTwoNumberStringWithUserSeparator(self):
        self.assertEquals(7, add("///\n2/5"))

    def testThreeNumberStringWithUserSeparator(self):
        self.assertEquals(15, add("//#\n2#5#8"))

    def testThreeNumberStringNewLineAndUserSeparator(self):
        self.assertEquals(15, add("//!\n2!5\n8"))

    @expected(ValueError)
    def testNegativesNotAllowed(self):
        add("2,-1")

    @expected(ValueError)
    def testNegativesNotAllowedWithUserSeparator(self):
        add("//&\n2&-1")

    def testIgnoreGreaterThan1000(self):
        self.assertEquals(1, add("1\n1001"))

    def testIgnoreGreaterThan1000WithUserSeparator(self):
        self.assertEquals(8, add("//$\n1\n1002$3$4$1005"))


if __name__ == '__main__':
    unittest.main()

Código:

#!/usr/bin/env python

def parse(s):
    return int(s) if s else 0

def tonums(lines, sep=','):
    return [parse(part) for line in lines for part in line.split(sep)]

def config(s):
    lines = s.split()
    if lines and lines[0].startswith('//'):
        return lines[1:], lines[0][2:]
    return lines, ','

def add(s):
    lines, sep = config(s)
    nums = tonums(lines, sep)
    negatives = [n for n in nums if n < 0]
    if negatives:
        raise ValueError('No se permiten negativos: %r' % negatives)
    return sum(n for n in nums if n <= 1000)

Al igual que en el caso de +Alcides Flores Pineda y +rodrigo salado anaya , el presente código no implementa el requerimiento de separadores de longitud arbitraria.

El código resultante es más largo que la versión en scheme/kawa, en algunos casos por la verbosidad propia de las comprensiones y generadores de Python, y en otras por las diferencias propias de la implementación, más que por cuestiones sintácticas.

A mi en lo particular me resulta muy ilustrativo ver las diferentes respuestas que se han generado para la misma kata en diversos lenguajes o incluso de personas distintas usando el mismo lenguaje.

La siguiente parte del ejercicio es: ejecutarlo en 15 o menos minutos todos los días por una semana y al cabo de ese tiempo, reflexionar:

  • ¿Mi solución a la kata está evolucionando cada vez que la ejecuto o por el contrario, repito los mismos pasos "como periquito"?
  • ¿Puedo encontrar alguna forma más elegante/clara/eficiente de resolver el problema o alguna parte de el mismo?
  • ¿Si la primera vez resolví el problema de forma, digamos, procedural, puedo intentar una solución orientada a objetos?, ¿funcional?
  • ¿Qué ventajas/desventajas tendrían dichas soluciones?

Saludos

4 de febrero de 2013

String Calculator en Kawa

Hace un par de días, un amigo nuestro compartió en su blog de Java México, su solución a la Kata String Calculator en Groovy. Esto me motivó a compartir también mi solución en Kawa y compararlas para ver si las mismas ideas se podrían implementar/traducir en/a Groovy.

Cabe mencionar que yo también me tardé mas de media hora en resolver el ejercicio (dos pomodoros de 20 min c/u), lo cual confirma la teoría que teníamos con un amigo sobre el tiempo que sería necesario para este ejercicio en un Coding-Dojo.

He aquí el código fuente en Scheme Kawa:
; -*- coding: utf-8; mode: Scheme -*-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Coding Kata String Calculator 

(require 'srfi-1 ) ;; List Library
(require 'srfi-13) ;; String Library
(require 'srfi-14) ;; Character-set Library

;; (define-simple-class StringCalculator ()
;;   ;; methods
;;   ((Add numbers ::String) ::int allocation: 'static

(define (Add numbers ::String) ::int
  (let* ((lstnum (filter (lambda (n)
                           (if (<= n 1000) #t #f))
                         (map string->number
                              (string-tokenize 
                               numbers
                               (char-set-adjoin char-set:digit #\-)))))
         (negativos (filter negative? lstnum)))
    (if (null? negativos)
        (apply + lstnum)
        (throw (java.lang.RuntimeException 
                (format #f 
                        "No se permiten negativos: ~s~%" 
                        negativos))) )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Pruebas Unitarias
(require 'srfi-64) ;; Testing Library

;;(define-alias Add StringCalculator:Add)

(test-begin "string-calculator")
;;(test-assert "Prueba no implementada" #f)
;;Req 1.1 y 1.2
(test-equal 0 (Add "Hola"))
(test-equal 0 (Add ""))
(test-equal "Suma '1'" 1 (Add "1"))
(test-equal "Suma '1,2'" 3 (Add "1,2"))
;;Req 2 y 3
(test-equal "Suma '1,2,3'" 6 (Add "1,2,3"))
(test-equal "Suma '1\n2,3'" 6 (Add "1\n2,3"))
;;Req 4
(test-equal 3 (Add "//;\n1;2"))
;;Req 6, 8 y 9
(test-equal 2 (Add "2,1001"))
(test-equal 6 (Add "//[*][%]\n1*2%3"))
;; pruebas del script de groovy
(test-equal 0 (Add "")) ;;assert 0 == "".sum()
(test-equal 0 (Add " ")) ;;assert 0 == " ".sum()
(test-equal 1 (Add "1")) ;;assert 1 == "1".sum()
(test-equal 1 (Add " 1")) ;;assert 1 == " 1".sum()
(test-equal 1 (Add " 1 ")) ;;assert 1 == " 1 ".sum()
(test-equal 1 (Add "  1  ")) ;;assert 1 == "  1  ".sum()
(test-equal 0 (Add "a")) ;;assert 0 == "a".sum()
(test-equal 3 (Add "1,2")) ;;assert 3 == "1,2".sum()
(test-equal 1 (Add "1, a")) ;;assert 1 == "1, a".sum()
(test-equal 2 (Add "a, 2")) ;;assert 2 == "a, 2".sum()
(test-equal 6 (Add "1,2,3")) ;;assert 6 == "1, 2, 3".sum()
(test-equal 6 (Add "//;\n1, 2, 3")) ;;assert 6 == "//;\n1, 2, 3".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5, \n'")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5, \n'".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5, \n', a, b; c' d")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5, \n', a, b; c' d".sum()
(test-equal 0 (Add ",;\n")) ;;assert 0 == ",;\n".sum()
(test-error  (Add "-1, 2, 3")) ;;try{ "-1, 2, 3".sum()
(test-error  (Add "-1, 2, 3, -4, -5")) ;;try{ "-1, 2, 3, -4, -5".sum() }catch(e){ println e}
(test-error  (Add "//;'\n1, 2, -3' -4' 5")) ;;try{ "//;'\n1, 2, -3' -4' 5".sum() }catch(e){ println e}

(test-end "string-calculator")


Notas:
  • Escribir una clase con un solo método (que además es estático) es una mala práctica. Aunque en este caso el requerimiento así lo estipule, he preferido dejar el método Add como una función ordinaria de Scheme y dejar el código de definición de clase y método comentados solo como ejemplo de como se hace en Kawa.
  • Agregué al final de las pruebas unitarias, la traducción a Scheme de las pruebas del script de Groovy para comprobar que el código cumple con los mismos requerimientos. E.d. Pasa las mismas pruebas

El suponiendo que código anterior se guarde en un archivo llamado "string-calc.scm", éste puede ser ejecutado desde la línea de comandos de la siguiente forma:
$ kawa -f string-calc.scm
Produciendo el siguiente resultado:
%%%% Starting test string-calculator  (Writing full log to "string-calculator.log")
# of expected passes      28
Hasta aquí la coding kata de hoy.

Saludos cordiales.

9 de febrero de 2012

TDD Cómo y porqué: Una guía para los no iniciados

Introducción

He estado practicando la disciplina del Desarrollo Dirigido por Pruebas o TDD ahora si y ahora no por los últimos ocho años. Quienes hayan leído algunos de mis posts anteriores, sabrán que no ha sido ni por mucho un proceso fácil ni mucho menos rápido.
Las excusas abundan, es simplemente demasiado fácil ceder y dejar de hacerlo. Desde los clásicos "en mi empresa no me lo permiten" y "mi jefe dice que me pagan por escribir funcionalidad, no pruebas" hasta los no tan infrecuentes "las pruebas son trabajo de QA, no mio" o "esta porquería es una mi3rd@, ¡es imposible de probar!". Lo sé porque yo mismo he estado en esas situaciones y ni una sola vez puedo decir que el resultado haya sido positivo para mi o para el proyecto.
El propósito de este blog es allanar un poco el camino para aquellos que estén considerando aprender TDD y posiblemente utilizarlo en su trabajo o en proyectos personales. Para mi no ha sido fácil y aún estoy aprendiendo. Si alguien puede sacar algo en claro de mis propias experiencias pasadas, creo que eso será más que suficiente para compensar el tiempo invertido en el mismo.

Este blog debe considerarse un trabajo en progreso. La idea del mismo comenzó hace probablemente tres o cuatro meses. Simplemente hay demasiados temas y ángulos para cubrir de una sola vez. El material disponible en la red, las anécdotas personales, las técnicas, los "pitfalls" (alguien, español por favor?), los "tips", etc. son suficientes como para llenar uno o varios libros así que no puedo esperar cubrir todas las bases en un solo post. Debido a ello, en este post me concentraré exclusivamente en el tema de las pruebas unitarias. Solo el tiempo dirá si soy capaz de llevar este proyecto a buen término :)

¿Qué es TDD?

TDD es una criatura extraña. Es simple de definir, pero su definición parece ir en contra del sentido común. Es sencilla de explicar, pero difícil de llevar a cabo. Y una vez que superas la resistencia intelectual inicial (the "WTF factor") y lo entiendes, es difícil de dominar.
Definición:
Es una disciplina que promueve el desarrollo de software con altos niveles de calidad, simplicidad de diseño y productividad del programador, mediante la utilización de una amplia gama de tipos de pruebas automáticas a lo largo de todo el ciclo de vida del software. El principio fundamental es que las pruebas se escriben antes que el software de producción y estas constituyen la especificación objetiva del mismo.
La primera parte de la definición suena todo miel sobre hojuelas. ¿Quién no quiere software confiable, bien diseñado y producido rápidamente?. Sin embargo, todo esto no viene gratuitamente; la palabra clave aquí es disciplina.
Disciplina:
Doctrina, instrucción de una persona, especialmente en lo moral. Observancia de las leyes y ordenamientos de la profesión o instituto.
Esto nos lleva a la conclusión de que si TDD es en efecto una disciplina, entonces no es algo que aplicamos "según nos vayamos sintiendo", más bien, es algo que debe formar parte integral de nuestra profesión o arte (según la perspectiva de cada quien).
La segunda parte de la definición viene con el primer "WTF" para muchos: Las pruebas se deben escribir antes que el software mismo. La primera impresión de muchos (incluyendo a yours truely) es "¿eh?, ¿y cómo demonios escribo una prueba para software que todavía no existe?".
Cuando aprendemos a programar, los más afortunados comienzan con algún lenguaje interpretado como Basic, Logo o Scheme (para los más veteranos) o Ruby y Python. Normalmente comenzamos con cosas simples como por ejemplo, sumar 2 y 3:
>>> 2 + 3
5
Intuitivamente pensamos "debe dar cinco", incluso antes de oprimir la tecla ENTER; y normalmente funciona o si no, entonces hay algo definitivamente mal con el lenguaje o con nuestro entendimiento del mismo. Posteriormente pasamos a cosas más complejas y/o sofisticadas, como por ejemplo:
>>> a = 2
>>> a + 3
5
>>> b = 3
>>> a + b
5
>>> def sum(a, b):
...   return a + b
...
>>> sum(2, 3)
5
Etcétera. Todo el tiempo verificamos que el resultado es el que esperamos, aunque "sabemos" que así debería ser. Cada vez que vemos el resultado que esperamos aparecer en la pantalla, aumenta nuestra autoconfianza, lo que nos motiva a seguir aprendiendo, a seguir programando. Este podría tal vez ser el ejemplo más básico de TDD.
Sin embargo, una vez que tomamos mayor confianza en nuestro dominio del lenguaje o la programación misma, comenzamos a escribir cantidades cada vez mayores de código entre una comprobación y la siguiente del resultado. Como "sabemos" --en realidad, creemos-- que nuestro código "esta bien", comenzamos a "optimizar el tiempo" escribiendo más y más código de una vez. Al poco tiempo, nos olvidamos de estas primeras experiencias, incluso tachándolas como "cosas de novatos".

Aprendiendo TDD

Fast forward al presente y nos encontramos a nosotros mismos tratando de aprender TDD. Nos conseguimos una copia de JUnit, NUnit, o el framework de moda para nuestro lenguaje de elección y comenzamos a seguir el tutorial que seguramente encontraremos en el sitio de este último. Los más afortunados probablemente tendrán integrada la funcionalidad directamente en su IDE.
A partir de aquí, estamos en la parte sencilla de nuestra curva de aprendizaje. En los próximos días comenzaremos a producir grandes cantidades de pruebas y no tardaremos en sentirnos cómodos con el proceso. Esto es lo más lejos que la mayoría llegamos en la curva y es aquí justamente donde comienzan los problemas.
Conforme comenzamos a intentar escribir pruebas para proyectos más complejos o incluso en el trabajo nos topamos con varios obstáculos en el camino:
  • Las pruebas se tornan difíciles de escribir, por lo que sentimos una desaceleración importante.
  • Corren lentamente, lo que nos volvemos renuentes a ejecutarlas frecuentemente.
  • Son frágiles, por lo que cambios aparentemente sin importancia en el código provocan que un montón de pruebas fallen.
  • Mantenerlas en forma y funcionando se vuelve complejo y consume tiempo.
Finalmente nos damos por vencidos, abandonamos completamente nuestras mejores intenciones y pensamos "Simplemente no vale la pena".
Estamos en la parte más pronunciada de nuestra curva de aprendizaje. Tal vez estamos produciendo muchas pruebas, y estamos obteniendo verdadero valor de las mismas. Sin embargo el esfuerzo para escribir/mantener estas mismas parece desproporcionado. Sin embargo, como cualquier otra habilidad que valga la pena adquirir, si en lugar de rendirnos seguimos adelante, eventualmente aprenderemos a cruzar a la parte de nuestra gráfica donde la pendiente de la curva se invierte y comenzamos a escribir pruebas más efectivas con un menor esfuerzo y a cosechar los beneficios de nuestra perseverancia.
Aprender a escribir bien y de mantener las pruebas toma tiempo y práctica. El propósito de este blog es, en parte para ayudar a acelerar un poco este proceso, de forma que no se tenga que escribir muchas pruebas basura, imposible de mantener antes de comenzar a ver la luz al final del túnel.

Las reglas de TDD

Robert C. Martin (también conocido como "Tío Bob"), es una de las autoridades en TDD. En varias ocasiones ha descrito el proceso en base a tres simples reglas:
  1. No está permitido escribir ningún código de producción sin tener una prueba que falle.
  2. No está permitido escribir más código de prueba que el necesario para fallar (y no compilar es fallar).
  3. No está permitido escribir más código de producción que el necesario para pasar su prueba unitaria.
Esto significa que antes de poder escribir cualquier código, debemos pensar en una prueba apropiada para él. Pero por la regla número dos, ¡tampoco podemos escribir mucho de dicha prueba! En realidad, debemos detenernos en el momento en que la prueba falla al compilar o falla un assert y comenzar a escribir código de producción. Pero por la regla número tres, tan pronto como la prueba pasa (o compila, según el caso), debemos dejar de escribir código y continuar escribiendo la prueba unitaria o pasar a la siguiente prueba.
Creo que esto se verá mejor con un pequeño ejemplo:
  • Escribimos suficiente de nuestra primera prueba para que falle
    from unittest import main, TestCase
    
    class TestPrimeFactors(TestCase):
        def testPrimesOf0(self):
            self.assertEquals([], factorsOf[0])
    
    if __name__ == '__main__':
        main()
    E
    ======================================================================
    ERROR: testPrimesOf0 (__main__.TestPrimeFactors)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "<stdin>", line 3, in testPrimesOf0
    NameError: global name 'factorsOf' is not defined
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    
    FAILED (errors=1)
  • Podemos ver obtenemos el error "NameError: global name 'factorsOf' is not defined". Esta es nuestra señal para detenernos y escribir la definición de factorsOf:
    def factorsOf(n):
        return []
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s
    
    OK
  • testPrimesOf0 pasa. Podemos continuar escribiendo código de prueba:
        def testPrimesOf0to1(self):
            self.assertEquals([], factorsOf(0))
            self.assertEquals([], factorsOf(1))
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s
    
    OK
  • Bien hasta aquí. Siguiente prueba:
        def testPrimesOf2(self):
            self.assertEquals([2], factorsOf(2))
    .F
    ======================================================================
    FAIL: testPrimesOf2 (__main__.TestPrimeFactors)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/home/alfredo/blog/tdd-rules.py", line 12, in testPrimesOf2
        self.assertEquals([2], factorsOf(2))
    AssertionError: Lists differ: [2] != []
    
    ----------------------------------------------------------------------
    Ran 2 tests in 0.029s
    
    FAILED (failures=1)
  • testPrimesOf2 falla. Hora de escribir código:
    def factorsOf(n):
        if n > 1:        return [n]    return []
    ..
    ----------------------------------------------------------------------
    Ran 2 test in 0.000s
    
    OK
  • testPrimesOf2 pasa. Siguiente prueba:
        def testPrimesOf2to3(self):
            self.assertEquals([2], factorsOf(2))
            self.assertEquals([3], factorsOf(3))
    
    ..
    ----------------------------------------------------------------------
    Ran 2 test in 0.000s
    
    OK
  • También pasa sin modificación. Pasamos a la siguiente prueba:
        def testPrimesOf2to4(self):
            self.assertEquals([2], factorsOf(2))
            self.assertEquals([3], factorsOf(3))
            self.assertEquals([2,2], factorsOf(4))
    .F
    ======================================================================
    FAIL: testPrimesOf2to4 (__main__.TestPrimeFactors)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "/home/alfredo/blog/tdd-rules.py", line 16, in testPrimesOf2to4
        self.assertEquals([2,2], factorsOf(4))
    AssertionError: Lists differ: [2, 2] != [4]
    
    ----------------------------------------------------------------------
    Ran 2 tests in 0.001s
    
    FAILED (failures=1)
  • Falla de nuevo. Hora de modificar el código de nuevo:
    def factorsOf(n):
        result, factor = [], 2    if n > 1:    while n > 1:
            return [n]        while n % factor == 0:            result.append(factor)            n /= factor        factor += 1    return []    return result
    ..
    ----------------------------------------------------------------------
    Ran 2 test in 0.000s
    
    OK
  • Pasa.
    
    
Obviamente he resumido el proceso un poco debido a limitaciones de espacio, pero creo que el proceso es claro.
Podemos ver que en realidad nunca escribimos mucho código de una sola vez. ¡Y de eso se trata precisamente! Es mucho muy similar al proceso descrito al principio de este post, cuando probábamos nuestro código interactivamente en el intérprete. Una iteración completa por todo el ciclo toma solo unos segundos o máximo un par de minutos. La retroalimentación se mantiene alta y esto nos motiva a seguir adelante con confianza y determinación. ¿Porqué? Simple: en todo momento, si seguimos este proceso durante todo el día, sabemos que nuestro sistema está funcionando. Incluso si comentemos un error hace solo un momento el sistema funcionaba correctamente. Si introducimos un bug, único que hace falta es oprimir Ctrl-Z unas cuantas veces y podremos regresar a nuestra barra verde. Y creo firmemente que eso es algo valioso.
Uno de los problemas fundamentales a los que me he enfrentado a través de los años al desarrollar software, es el no entender bien lo que estoy haciendo en un momento dado. Hay veces que simplemente estoy tratando de entender una nueva API o sistema y debo escribir código para implementar nueva funcionalidad. Así que copio y pego código que encuentro en algún libro o sitio de internet y trato de hacerlo funcionar. Un cambio aquí, otro allá hasta que aparentemente funciona. El problema es que no entiendo lo que acabo de hacer. Si el código falla en QA o incluso un par de semanas después de haberlo escrito, realmente no tengo mucha idea de porqué. De hecho, no tengo idea de porqué funcionó cuando lo puse ahí en primer lugar.
Al seguir de forma disciplinada estas tres simples reglas, nunca paso demasiado tiempo sin saber si lo que hago funciona o no. Y como nunca escribo demasiado código, puedo entender plenamente cómo y porqué funciona.

Escribiendo pruebas unitarias efectivas

Roy Osherove en The Art of Unit Testing dice que las buenas pruebas tienen tres propiedades comunes: son legibles, confiables y fáciles de mantener. Una cuarta propiedad que yo agregaría es "rapidez", por razones que discutiremos más adelante.

Rapidez

Una prueba unitaria efectiva debería ejecutarse en milisegundos, no en segundos. Si a una prueba tarda más que algunos cientos de milisegundos en ejecutar, es probable que debamos considerarla demasiado lenta. Como en todo, hay excepciones, pero esta es una buena regla a seguir. A continuación explico porqué.
Una suite de pruebas puede llegar a contener decenas o incluso cientos de pruebas, organizadas en clases, cada una enfocándose a un aspecto particular del código. Si una prueba se ejecuta en un segundo (1000 milisegundos) y tenemos cien de ellas, tendremos que esperar más de dos minutos para conocer el resultado de las mismas. Correr la suite se convierte en si mismo en una interrupción que altera nuestro ritmo de trabajo: La mente se distrae y para cuando finalmente tenemos los resultados, debemos de "vaciar la pila" nuevamente y reingresar el contexto que teníamos unos segundos antes de ejecutar la suite.
Si hacemos esto una y otra vez en el transcurso del día (a la mayoría de los desarrolladores se les complica mantener su atención por más de algunos segundos antes de perder el interés), entonces comenzaremos a evitar el correr nuestras pruebas, lo cual deberíamos hacer cada vez que cambiamos "algo" en el código. Y si esto es así, entonces perdemos la confianza en nuestros cambios y en nosotros mismos. Regresamos al ritmo "tradicional" y finalmente puede llegar a parecernos "más fácil" abrir el depurador de nuestro IDE que dar un par de pasos hacia atrás hasta el punto en que todo aun funcionaba bien.
Todo ello sin considerar el tiempo perdido. No solo el tiempo de ejecución de las pruebas mismas, sino el tiempo que nos toma volver a entrar en el contexto mental que teníamos justo antes.
Una sesión de TDD en realidad debería transcurrir como los ejemplos interactivos al principio de este post. Escribimos algo de código y deberíamos obtener retroalimentación casi inmediatamente. Cada vez que corremos la suite "en verde", aumenta nuestra confianza, en nuestro código, en nuestra suite y en nosotros mismos, lo cual nos mantiene altamente motivados para seguir adelante.
Volver al ciclo tradicional de modificar/compilar/debuguear destruye esa motivación. Si probar un cambio de una sola linea nos lleva 5 minutos de en el depurador, nuestra motivación se va a los suelos y se convierte en una excusa para alargar los tiempos de desarrollo casi infinitamente.
Un componente fundamental en la construcción de una suite de pruebas es la habilidad de construirla a partir de subconjuntos más pequeños y enfocados. Es importante ser capaz de probar el sistema completo oprimiendo solo un botón (o con un solo comando en la shell del sistema), pero igualmente importante es poder ejecutar únicamente las pruebas para la clase o el sub-sistema que estamos probando en este momento. La mayoría de los frameworks de la familia XUnit tienen esta capacidad. Se pueden crear suites pequeñas y estas a su vez, incluirlas en suites mayores.

Legibilidad

Una prueba legible es aquella que revela su propósito o razón de ser de forma clara. Básicamente, qué es lo que la prueba ha de demostrar. Una parte importante de la legibilidad de una prueba consiste simplemente en darle un nombre apropiado. Si está probando una pila, por ejemplo, entonces no llamemos nuestras pruebastestStack_01, testStack_02, etc. No solo son nombres bobos (por decir lo menos) sino que lo único que revelan es que en alguna parte debe haber algún objeto o función llamado "Stack" involucrado. En cambio, elegir nombres que reflejen el comportamiento útil observable que el código debiera exhibir. Por ejemplo, testElementosGuardadosSonRegresadosEnOrdenInverso es un nombre que describe un comportamiento observable de las pilas: los elementos colocados al principio son los últimos en ser devueltos.
Es conveniente considerar que los nombres de las pruebas forman parte de la documentación del comportamiento de la Unidad de Código Bajo Prueba. Cuando llega el momento de implementar una nueva clase, a menudo encuentro útil comenzar con una lista inicial de las pruebas que quiero escribir (no siempre lo hago, pero a veces resulta indispensable). Puedo usar esta lista como un primer borrador de la especificación de la clase en cuestión, por ejemplo:
  • No debe ser posible crear una flootsam sin una jetsam asociada.
  • Si la flootsam no es persistente, no puede contener hijos.
  • Una flootsam con hijos no puede ser cancelada.
  • etc.
Esta lista más adelante puede convertirse en la base de los nombres de nuestras pruebas.
Cuando las pruebas llevan el nombre de una conducta observable, esta tiende naturalmente a reflejar unicamente este aspecto del código. Es aceptable tener más de un assert, siempre que estos se refieran a una sola cosa, generalmente a un solo objeto.
Encontrar el justo equilibrio entre tener el código de inicialización dentro de las pruebas, en una fábrica o en un método setupdedicado, es también un elemento importante de la legibilidad. Es importante reducir el volumen del código en las pruebas, pero también queremos que sea evidente lo que la prueba está haciendo. Es fácil caer en la trampa de ocultar muchos detalles en los métodos de inicialización o de fábrica, por lo que un lector tiene que buscar estos métodos para poder entender la prueba. El principio DRY, a veces se encuentra firmemente grabado en la consciencia de los buenos programadores. Sin embargo, es perfectamente aceptable tener un poco más de redundancia, mientras que el propósito se mantenga claro.
Esto último no quiere decir que podemos ignorar las reglas y escribir nuestras pruebas de forma descuidada. Nuestras pruebas son parte esencial de nuestro código. Son tan importantes como el código de producción (o de acuerdo con Robert C. Martin, son aún más importantes). Por lo tanto es necesario poner tanto esmero en su manufactura como el que pondríamos en la demo que haremos la próxima semana frente al cliente.

Confiabilidad

Una prueba confiable es la que falla o pasa de forma determinista. Las pruebas que dependen si la computadora está configurada correctamente, o cualquier otro tipo de variables externas, no son confiables, porque no es posible saber si una falla significa que el equipo no está configurado correctamente, o si el código contiene errores.
Estas pruebas que dependen de variables externas son en realidad pruebas de integración, y se deben poner en un proyecto por separado, junto con alguna documentación sobre la forma de ponerse en marcha. Esto es deseable, ya que este tipo de pruebas normalmente se ejecutan mucho más lentamente que las pruebas unitarias típicas, por lo que al estar separadas, no impedirán que ejecutemos nuestras pruebas unitarias tan frecuentemente como deseemos/necesitemos.
Una variable externa es cualquier cosa sobre la que no tenemos control directo: el sistema de archivos, bases de datos, el tiempo, el código de terceros, etc.
En cuanto al tiempo, basta con crear algunas instancias tipo fecha con un instante fijo, en lugar un indeterminado "tiempo actual" y olvidarnos del asunto. En una prueba unitaria, deberíamos utilizar exactamente los mismos datos de prueba cada vez, pero si la prueba dependiera de un valor como DateTime.Now, entonces efectivamente sería una prueba diferente cada vez que se ejecute.
En algunas ocasiones especiales, es imposible evitar el tener una prueba indeterminable sin importar cuanto nos esforcemos. Martin Fowler y otros recomiendan en primer lugar, aislar estas pruebas. Lo último que queremos es acostumbrarnos a ver fallar pruebas en nuestra suite. Una barra roja para nosotros siempre debe ser una señal de alarma. No importa que podamos reconocer la prueba por su nombre. El punto de usar pruebas automáticas es precisamente no tener que inspeccionar visualmente los resultados para darlos o no por buenos. Si esto sucede, ¡podemos pasar por alto un fallo real sin notarlo! Otro punto es el analizar si una aproximación probabilística es útil en estos casos. Si los resultados de la prueba se encuentran acotados dentro de un margen de tolerancia, es posible eliminar la incertidumbre hasta un grado aceptable para nuestros propósitos.

Mantenibilidad

Una prueba fácil de mantener es aquella que no "se rompe" fácilmente cuando se les da mantenimiento. Un bajo acoplamiento es probablemente el factor más importante para la facilidad de mantenimiento. El uso de Métodos de fábrica nos permite desacoplar nuestras pruebas de los constructores de clase, que tienden sufrir cambios en sus listas de parámetros más a menudo que otros métodos.
Dar nombres significativos a nuestras pruebas también es importante para el mantenimiento, así como la legibilidad. Cuando se puede deducir a partir del nombre lo que la prueba está tratando de comprobar, se puede ver si en realidad el código hace lo que se dice que está haciendo. Puede asegurarse que las pruebas mantienen su comportamiento, incluso cuando hay cambios en el API que utiliza.
Cuando las pruebas se pueden leer, entonces se vuelven más fáciles de mantener. Cuando las pruebas se pueden mantener, entonces es probable que en de hecho, se les dé mantenimiento. Cuando se sabe que las pruebas se mantienen, y se puede inferir lo que están comprobando, entonces es posible confiar en ellas como en la red de seguridad que se supone que son.

Cookbook

Aprender

  • Escribe muchas pruebas (tantas como puedas). Familiarizate con el ritmo y las reglas de TDD. Comienza con algo sencillo (¡pero no te detengas ahí!)
  • Cuando encuentres algo que no sabes como probar, apóyate en un compañero. Si no programas en parejas, consulta con un colega. Recolecta ideas de diversas fuentes.
  • Sé persistente y no te rindas. Si quieres obtener los frutos, debes primero poner el trabajo duro.
  • Nunca dejes de aprender. Lee libros (una lista al final de este post), blogs, revistas, etc. Los proyectos de Código Abierto son una excelente fuente de aprendizaje.
  • Conforme escribas más y más pruebas, comienza a organizarlas en suites y asegúrate que estas puedan ejecutarse de forma individual o colectiva, según sea necesario. ¡La organización también es una habilidad que hay que aprender!

Prácticas para el día a día

  • Es recomendable probar una unidad de código solo a través de su API pública (y en términos prácticos, "protegido" es efectivamente público). Al hacer esto, obtenemos un mejor aislamiento de los detalles específicos de la implementación.
  • Evita a toda cosa colocar lógica en el código de prueba (if-then, switch/case, etc). Donde hay lógica, hay la probabilidad de introducir bugs, ¡y definitivamente no queremos bugs en nuestras pruebas!
  • Evita los "números mágicos". Esto te permitirá entender porqué en una prueba particular un método regresa un valor dado o porque se pasa un cierto valor como parámetro y no otro diferente.
  • Evita calcular el valor esperado, ya que podríamos terminar duplicando el código del producción, incluyendo cualquier error que este pudiera tener. Preferiblemente, calcula el resultado esperado manualmente (y revisalo por lo menos un par de veces) y colócalo como una constante.
  • Evita compartir estado entre pruebas. Debe ser posible ejecutar las pruebas en cualquier orden o incluso, ejecutar una prueba dentro de otra prueba. Mantener las pruebas aisladas de las demás también es un factor indispensable para la confiabilidad y mantenibilidad de las mismas.
  • Sin importar como lo veas, ninguna cantidad de comentarios puede sustituir un código claro. Si una prueba se convierte en un desastre, reescríbela.
  • Si no es posible determinar lo que una prueba está haciendo, es probable que en realidad esté verificando múltiples cosas: hazla pedazos y convierte cada uno en su propia prueba individual.
  • Frecuentemente los errores en el código de pruebas se esconden en los métodos de inicialización. Mantener este código simple y compacto puede ser un gran paso para la mantenibilidad del código.
  • Una unidad de código puede necesitar operar en circunstancias escenarios variables. Esto puede llevar a que el código de inicialización se convierta rápidamente en un desastre. Crea fixtures o incluso casos de prueba especializados para cada escenario.
  • Nunca escatimes en claridad. Si es necesario, convierte cada escenario en una clase de prueba individual.
  • Si al probar una parte de tu código parece que requieres tener la mitad o más del sistema presente, verifica el alcance de la misma. ¿Estás probando una sola cosa?
  • Si una parte del código es particularmente resistente a tus esfuerzos de probarla, voltea al código en busca de problemas en el diseño del mismo. Un código fácil de probar frecuentemente está débilmente acoplado con el resto del sistema, es altamente cohesivo y sigue los principios fundamentales del diseño de software.
Espero como siempre que haya sido de utilidad.

14 de octubre de 2011

TDD is not a testing technique, it's a design technique

Esta es una pequeña entrada para comentar sobre la reciente publicación de Mark Seemann "The TDD Apostate".

En realidad, no es que quiera debatir lo expresado por Seeman en su blog. En realidad, estoy de acuerdo en la gran mayoría de lo que ahí expresa. Sin embargo, un sentimiento de incomodidad me recorría mientras leía su artículo: "claro, ¡eso es obvio!" decía para mis adentros una y otra vez. Y la razón de ello es bastante clara ahora: TDD es una herramienta valiosísima para un desarrollador. Es quizás la técnica más importante que ha llegado al mainstream del desarrollo de software en los últimos 10 años—si tan solo más personas la usáramos—pero ¡no es una bala de plata!

Parece ser que algunas personas siguen con la obsesión de buscar la nueva técnica o tecnología o invento o aceite de víbora que cure todos los males del desarrollo de software–seguramente que los gerentes y directivos de las grandes empresas tienen puestas sus esperanzas en ello. Otras por el contrario, desean que estos nunca se resuelvan y por el contrario, hacer mucho dinero vendiendo aceite de víbora. Ambos son intereses legítimos. Sin embargo, también habemos muchos programadores que vivimos soñando con estos remedios mágicos. Pensamos en lo difícil que es nuestro trabajo y en cómo nos gustaría descubrir un truco nuevo que nos permitiera resolver nuestros problemas y dedicar nuestro hermoso tiempo a navegar por internet y a jugar con nuevos gadgets.

Sin embargo, es preciso no perder de vista que el desarrollo de software es una actividad inherentemente humana y por consiguiente, difícil de automatizar y simplificar. Es ante todo una actividad creativa, que de algún modo se encuentra en algún punto entre escribir una novela de misterio y elaborar los planos de un rascacielos.

El punto del sr. Seeman es que la Programación Dirigida por Pruebas por sí misma no es una buena forma de diseñar software: de acuerdo. Recuerdo hace años haber leído en algún lugar que los impulsores de XP predicaban el no-diseño, que todos los males se podían arreglar mediante YAGNI y refactoring y no-sé-qué otras estupideces por el estilo. Cualquiera que haya leído el libro de Martin Fowler sabrá que eso no es verdad ni de lejos.

Sin embargo, muchos pseudo-programadores y otros perezosos fuimos seducidos por el llamado de la sirena: nos dejamos embaucar por las falsas promesas de aquellos que ni entendían ni les interesaba la verdadera intención tras el movimiento ágil y hallamos en estas frases propagandísticas una justificación para nuestro descuido. Otros solo entendimos el mensaje a medias, lo suficiente para poder hablar con grandilocuencia de ello, pero sin jamás llegar a aplicarlo. Algunos más escuchamos los cantos de júbilo de aquellos que abrazaron con entusiasmo el movimiento y fueron los primeros en superar los problemas iniciales y recoger sus primeros frutos. Aquello parecía ser demasiado bueno para ser cierto. Y así era.

Si bien es cierto que la TDD nos aporta un sinnúmero de ventajas, no se trata de ninguna panacea. Kent Beck nos dá una pista de la importancia de la TDD al comentar que "si pudiera convencer a los equipos de desarrollo de adoptar solo dos prácticas de XP, estas serían TDD y la programación en parejas".

El maestro Ward Cunningham dijo hace años:

"Programar las pruebas primero no es una técnica de verificación, es una técnica de diseño"

Fijémonos que nunca menciono que fuera "la única técnica de diseño" ni "la técnica definitiva de diseño". Y la razón es clara: El escribir primero la prueba y después el código nos permite expresar de forma concreta las ideas que tenemos acerca del código que estamos a punto de escribir. Es como dibujar un pequeño boceto en una servilleta, excepto que este boceto es ejecutable y nos indica inmediatamente si al poner en marcha la idea que expresamos lo hemos hecho de forma correcta de acuerdo al entendimiento que tenemos del problema hasta ese momento.

Pero por otro lado, ¿qué arquitecto en sus cabales construiría un edificio basado únicamente en un boceto en una servilleta? Al hacer TDD de forma natural nos enfocamos de forma intensa en la clase/función/historia que estamos implementando en el momento. Eso significa que si es lo único que hacemos para diseñar nuestro software, inevitablemente terminaremos con un montón de bocetos en servilletas que no necesariamente forman un plano completo de nuestro edificio. O por lo menos, tal vez no uno en el cual nos gustaría vivir.

Aunque los ingenieros, arquitectos, médicos, etc. tengan a su disposición una gran cantidad de técnicas y tecnologías que faciliten e incluso automaticen las partes más mecánicas de su trabajo, ninguno de ellos puede olvidarse de toda la base teórica que durante tantos años y con tanto esfuerzo adquirieron. No es casualidad que algunas de figuras más notables impulsores de la TDD como Kent Beck, Ward Cunningham, Martin Fowler o Robert C. Martin sean también figuras destacadas en el ámbito de los principios y patrones de diseño.

Si la TDD fuera suficiente, no haría falta conocer los principios SOLID, los patrones de diseño de los GoF o los patrones GRASP, entre muchos otros.

Si, como desarrolladores debemos abrazar la TDD o como mínimo, alguna otra disciplina equivalente como por ejemplo el Diseño por Contrato (DbC). Hoy en día decir que practicamos TDD debería ser tan obvio como decir "si, cuando programo escribo código", debe ser una práctica core de nuestro oficio. A nadie debería sorprender, e incluso me atrevo a decir que a nadie debería enorgullecer el decir "yo practico TDD", pero si debería ser motivo de vergüenza lo contrario.

Sin embargo, TDD es solo el inicio. Recordemos que XP consta de 12 prácticas (o 13, dependiendo de la edición del libro que tengan):

La programación en parejas, el diseño simple, la metáfora, el refactoring, la propiedad colectiva del código, todas son prácticas que contribuyen a mantener la visión a gran escala del sistema en perspectiva, aún cuando nos concentremos en una sola unidad de software a la vez. Si no estamos en un proyecto/equipo que practique XP, esto hace aún más apremiante que debamos hacer nuestra tarea y conocer más allá de los meros detalles sintácticos de mi código, si no su participación y sus consecuencias como parte integral de un todo. Como se menciona en GoF:

"Los Patrones de Diseño... proveen puntos de llegada para nuestros refactorings."

¿Cómo sabremos cuando hemos arribado a nuestro destino si no sabemos cual es este destino?

En resumen: no, TDD no es una técnica completa ni infalible ni mucho menos a prueba de tontos para el diseño de software; es eso sí, una técnica valiosísima para diseñar unidades de código, mantenerlas limpias, cohesivas y débilmente acopladas con el resto del mundo. ¿Acaso no son estas propiedades deseables del todo?, ¿Acaso no son estas retribución más que suficiente a la disciplina que aplicamos todos los días al practicar TDD?

Citando a Michael Feathers:

He encontrado [también] que al tratar de escribir Pruebas Unitarias en proyectos existentes que no son de XP, para proporcionar un andamiaje para el cambio, se deshierban las dependencias, los problemas de instanciamiento, etc. Sólo puedo creer que si cada clase se desarrolla primero en un arnés de prueba, donde todas las dependencias explícitas, las cosas irán mejor. Ha sido mi experiencia en las cosas más bien pequeñas que he hecho, siguiendo el ejemplo de XP.

Saludos

30 de agosto de 2011

Pepe Grillo y TDD

Hace varias semanas tuve una conversación franca y abierta con mi amigo Alcides: el único tipo de conversación que es posible tener con él. En ella, discutimos las dificultades de su actual proyecto y de cómo hace falta una implementación más formal de algunas mejores prácticas, en especial las pruebas unitarias. Durante nuestra charla yo argumentaba "pero, ¿porqué no usas TDD?" a lo que el me respondía con varias razones:

  • Presiones de tiempo
  • La curva de aprendizaje del equipo de desarrollo
  • El estado actual del código
  • etc

Todas razones válidas, pero sin embargo insuficientes para justificar el no-uso de lo que ambos sabemos ayudará al proyecto. Es como ir al doctor, recibir la receta, surtirla y después negarse a tomar la medicina diciendo que estamos demasiado ocupados para hacerlo.

Ante esto, le recordé una frase del Robert "Tio Bob" Martin:

"Es durante las crisis que más debemos aferrarnos aún a nuestras prácticas y nuestras disciplinas"

Está claro: si el ingeniero civil que está construyendo nuestra casa comienza a tener problemas de tiempo, lo que menos queremos que haga es que empiece a dejar de lado los códigos de construcción, los cálculos, las medidas de seguridad, las mejores prácticas, etc., para ahorrar tiempo, dinero o porque son muy complicadas. Después de todo, si lo hace corremos el riesgo de que la casa se nos venga encima.

Tiempo después, yo mismo entré en modo de emergencia en mi propio proyecto y mientras estaba en ello, Alcides pasó a visitarme. Inmediatamente le mostré el código que acababa de escribir para que me diera su opinión, a lo que él replico:

- "bien, veamos... ¿dónde están las pruebas unitarias?" a lo que por supuesto respondí - "No hay. Este proyecto no me pertenece y mi líder me ha comentado que no tenemos tiempo para eso y debemos concentrarnos en escribir el código tan rápido como podamos: ¡Esto es una crisis!"

Por toda respuesta, recibí una mirada significativa de parte de mi amigo y segundos después la misma frase que le había dicho yo tiempo antes:

"Es durante las crisis que más debemos aferrarnos aún a nuestras prácticas y nuestras disciplinas"

Como dicen en mi pueblo: me mató el gallo en la mano. En ese momento Alcides fue como Pepe Grillo diciéndome que estaba faltando a mi propia conciencia, que estaba dejando que circunstancias externas me detuvieran de hacer mi trabajo de la mejor forma que se y la única que considero ética.

Siempre es valioso tener a alguien que pueda hacer eso por nosotros en momentos que por estrés, flojera o hastío dejemos de hacer caso a la propia conciencia.

Saludos

P.D. Me complace informar que tanto Alcides como un servidor hemos vuelto al buen camino y nos sentimos mucho mejor...