Mostrando entradas con la etiqueta programación. Mostrar todas las entradas
Mostrando entradas con la etiqueta programación. Mostrar todas las entradas

20 de noviembre de 2017

Redirección fuertemente tipada en ASP.NET MVC

Recientemente volví a caer en un error de los que considero de primer año: cambié la firma de un método de acción en un controlador de MVC y olvidé buscar el 100% de todos los lugares que redirigían hacia él. Por supuesto, esto causó un error --que afortunadamente fue encontrado en QA-- ya que el código en cuestión (claro está) no tenía pruebas de ninguna clase.

El código en cuestión era similar al siguiente:

public ActionResult Foo()
{
  ...
  return RedirectToAction(
      "SomeAction", "SomePlace",
      new { usr = "Username", key = blah });
}

El problema es que fue necesario agregar un parámetro al método SomePlaceController.SomeAction, pero absolutamente nada en el código me advirtió que había nada más que cambiar además de la función de JavaScript que estaba modificando en ese momento. Fue la gota que derramó el vaso: desde que he estado dando mantenimiento a código legacy en ASP.NET MVC, no sé cuantas veces me ha sucedido este mismo problema y ya era hora de hacer algo al respecto, así que decidí que ya que estoy trabajando con C#, tal vez podría utilizar su sistema de tipos a mi favor. Lo que me gustaría poder hacer sería algo así:

public ActionResult Foo()
{
  ...
  return RedirectToAction<SomePlaceController>(
    x => x.SomeAction("Username", blah, 125));
}

Por supuesto, no existe una definición de RedirectToAction como esta, pero es relativamente fácil de definir usando expresiones de linq:
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;
using System.Web.Routing;

using RouteValue = System.Tuple<string, object>;

public static class RedirectTo
{
  #region Public Methods

  private const string InvalidTarget =
    "El argumento debe ser una invocación a un método del controlador.";

  private const string UnsupportedExpression = "Expresion no soportada";

  #endregion

  #region Public Methods

  public static RedirectToRouteResult RedirectToAction<T>(
    this T controller, Expression<Action<T>> selector)
    where T : Controller => Action(selector);

  public static RedirectToRouteResult Action<T>(
    Expression<Action<T>> selector) where T : Controller
  {
    if (!(selector.Body is MethodCallExpression))
    {
      throw new ArgumentException(InvalidTarget, nameof(selector));
    }

    var methodEx = (MethodCallExpression)selector.Body;

    var routeValues = MakeRouteValues(
      controllerName: GetControllerName<T>(),
      actionName: GetActionName(methodEx),
      arguments: GetArguments(methodEx));

    return new RedirectToRouteResult(routeValues);
  }

  #endregion

  #region Methods

  private static string GetControllerName<T>() where T : Controller
  {
    const string Suffix = "Controller";

    var name = typeof(T).Name;

    return name.EndsWith(Suffix)
      ? name.Substring(0, name.Length - Suffix.Length)
      : name;
  }

  private static string GetActionName(MethodCallExpression expression)
    => expression.Method.Name;

  private static RouteValue[] GetArguments(MethodCallExpression expression)
  {
    var names = expression.Method.GetParameters().Select(o => o.Name);
    var values = expression.Arguments.Select(GetValueOf);

    return names
      .Zip(values, (n, v) => new RouteValue(n, v))
      .Where(o => o.Item2 != null)
      .ToArray();
  }

  private static object GetValueOf(Expression expression)
  {
    if (expression is MemberExpression)
    {
      return GetValueOf((MemberExpression)expression);
    }
    else if (expression is ConstantExpression)
    {
      return GetValueOf((ConstantExpression)expression);
    }

    throw new InvalidOperationException(UnsupportedExpression);
  }

  private static object GetValueOf(MemberExpression expression)
  {
    var container = GetValueOf(expression.Expression);

    if (expression.Member is FieldInfo)
    {
      return ((FieldInfo)expression.Member).GetValue(container);
    }
    else if (expression.Member is PropertyInfo)
    {
      return ((PropertyInfo)expression.Member).GetValue(container);
    }

    throw new InvalidOperationException(UnsupportedExpression);
  }

  private static object GetValueOf(ConstantExpression expression)
    => expression.Value;

  private static RouteValueDictionary MakeRouteValues(
    string actionName, string controllerName, params RouteValue[] arguments)
  {
    var args = new RouteValue[]
      {
        new RouteValue("action", actionName),
        new RouteValue("controller", controllerName),
      };

    return new RouteValueDictionary(args
      .Concat(arguments)
      .ToDictionary(o => o.Item1, o => o.Item2));
  }

  #endregion
}

Con esto, el sintaxis que obtenemos es muy similar al que queríamos:
public ActionResult Foo()
{
  ...
  return RedirectTo.Action<SomePlaceController>(
    x => x.SomeAction("Username", blah, 125));
}

o incluso
public ActionResult SomeAction(
  string username, Guid key, long token)
{
  var credentials = GetCredentials(username, token);
  ...
  return this.RedirectToAction(x => x.Continue(username, credentials));
}

Con esto, me aseguraré de que la próxima vez que necesite agregar un parámetro al un método de un controlador, no pueda hacerlo sin tener por lo menos que revisar cualquier redirección que haya hacia el mismo (por lo menos en código compilado del lado del servidor).

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

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...

19 de julio de 2011

Kata: Búsqueda Binaria

El propósito de la Kata es sencillo: practicar.

  • Los músicos practican escalas, arpegios, acordes, estudios, piezas completas, etc.

  • Los artistas marciales practican los movimientos individuales una y otra vez y después los combinan en secuencias de ataque-defensa-contra.

  • Los programadores... esperamos hasta la hora de la verdad... hmmm... tal vez no es la mejor estrategia.

El propósito del Kata no es buscar la solución a un problema. De hecho, la mayoría están basados en problemas simples, que han sido resueltos y documentados ampliamente en los libros de introducción a la ciencia de la computación.

Más bien, el valor de practicar un kata es practicar y afinar nuestras habilidades y disciplinas.

¿Cuantas veces nos hemos dicho a nosotros mismos que ahora si escribiré pruebas para el código de mi próximo proyecto?

¿O cuantas veces nos hemos prometido que ahora si vamos a aprender a usar correctamente los atajos de teclado del nuevo IDE que estamos comenzando a usar?

Dado que los kata son ejercicios controlados, sobre problemas cuya solución ya conocemos, nos dan la oportunidad perfecta para concentrarnos no tanto en la parte de descubrir o aprender el algoritmo, sino las partes mecánicas del proceso.

A continuación, procedo a desarrollar un kata muy básico: la búsqueda binaria.

Mi propósito para esta sección es hacerlo paso a paso siguiendo estrictamente el método de Desarrollo Dirigido por Pruebas (TDD por sus siglas en ingles)

El ritmo del TDD:

  • Fallo
  • Éxito
  • Refactorizar

También conocido como "Red, Green, Refactor!".

Las tres leyes del TDD:

  1. No se permite escribir nada de código de producción hasta no tener una prueba que falle (y no compilar es fallar).
  2. No se permite escribir una prueba más allá de lo necesario para que falle.
  3. No se permite escribir más código de producción que el necesario para pasar la prueba.

Escribimos nuestra primera prueba: El caso degenerado.

def test_bsearch():
   assert_equal(-1, bsearch(3, []))

if __name__ == '__main__':
   test_bsearch()

puesto que no existe la función bsearch, obtenemos un error:

Traceback (most recent call last):
 File "chop.py", line 64, in <module>
   test_bsearch()
 File "chop.py", line 61, in test_bsearch
   assert_equal(-1, bsearch(3, []))
NameError: global name 'bsearch' is not defined

A continuación hacemos lo mínimo necesario para corregir el error:

def bsearch(what, seq):
   pass

Ahora tenemos:

Traceback (most recent call last):
 File "chop.py", line 67, in <module>
   test_bsearch()
 File "chop.py", line 64, in test_bsearch
   assert_equal(-1, bsearch(3, []))
 File "chop.py", line 47, in assert_equal
   assert expected == actual, message
AssertionError: -1 was expected, but got None

Ahora hacemos lo más simple que pueda funcionar para lograr que pase la prueba:

def bsearch(what, seq):
   return -1

Con esto es bastante (si, algo bobo será, pero tengamos paciencia. Apenas estamos comenzando). Ahora escribamos la siguiente prueba:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))

Pasa sin hacer nada. Sigamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

Falla. ¿Qué es lo más simple que podemos hacer para que pase, junto con las dos anteriores?

def bsearch(what, seq):
   if len(seq):
       if seq[0] == what:
           return 0
   return -1

La prueba pasa. Sigamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

Las pruebas 4 y 5 pasan sin modificación. La 6 falla, sin embargo.

def bsearch(what, seq):
   if len(seq):
       if seq[0] == what:
           return 0
       if seq[1] == what:
           return 1
   return -1

¡¡Woops!!

Traceback (most recent call last):
 File "chop.py", line 79, in <module>
   test_bsearch()
 File "chop.py", line 71, in test_bsearch
   assert_equal(-1, bsearch(3, [1]))
 File "chop.py", line 65, in bsearch
   if seq[1] == what:
IndexError: list index out of range

Claro, debemos tomar en consideración las dimensiones de la lista:

def bsearch(what, seq):
   start, end = 0, len(seq) - 1
   if start <= end:
       if seq[start] == what:
           return start
       elif seq[end] == what:
           return end
   return -1

Listo. Continuamos:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(-1,  bsearch(2, [1, 3]))
   assert_equal(-1,  bsearch(4, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

   assert_equal(0,  bsearch(1, [1, 3, 5]))
   assert_equal(1,  bsearch(3, [1, 3, 5]))

Ahora el problema es que el elemento que buscamos se encuentra justo en medio de la lista. Podríamos hacer algo bobo como if seq[1] == what: return 1, pero eso no nos lleva a ningún lado. Recordemos que...

"Conforme las pruebas se vuelven más específicas, el código se vuelve más genérico."
def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   if start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[start] == what:
           return start
       if seq[end] == what:
           return end
   return -1

Siguiente prueba:

def test_bsearch():
   assert_equal(-1, bsearch(3, []))
   assert_equal(-1, bsearch(3, [1]))
   assert_equal(0,  bsearch(1, [1]))

   assert_equal(-1,  bsearch(0, [1, 3]))
   assert_equal(0,  bsearch(1, [1, 3]))
   assert_equal(1,  bsearch(3, [1, 3]))

   assert_equal(0,  bsearch(1, [1, 3, 5]))
   assert_equal(1,  bsearch(3, [1, 3, 5]))
   assert_equal(2,  bsearch(5, [1, 3, 5]))
   assert_equal(-1, bsearch(0, [1, 3, 5]))
   assert_equal(-1, bsearch(2, [1, 3, 5]))
   assert_equal(-1, bsearch(4, [1, 3, 5]))
   assert_equal(-1, bsearch(6, [1, 3, 5]))

Esto se está comenzando a poner algo redundante... tiempo de refactorizar:

def impares(cuantos):
   limite = 2 * cuantos
   return [indice for indice in xrange(1, limite, 2)]

def pares(cuantos):
   limite = 2 * (cuantos + 1)
   return [indice for indice in xrange(0, limite, 2)]

@trace
def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   if start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[start] == what:
           return start
       if seq[end] == what:
           return end
   return -1

def test_bsearch(limite):
   for cuantos in xrange(limite):
       lista = impares(cuantos)
          
       for indice, valor in enumerate(lista):
           assert_equal(indice, bsearch(valor, lista))

       for not_found in pares(cuantos):
           assert_equal(-1, bsearch(not_found, lista))

if __name__ == '__main__':
   test_bsearch(4)

Bien, hasta este punto todas las pruebas pasan:

$ python chop.py
bsearch(0, []) -> -1
bsearch(1, [1]) -> 0
bsearch(0, [1]) -> -1
bsearch(2, [1]) -> -1
bsearch(1, [1, 3]) -> 0
bsearch(3, [1, 3]) -> 1
bsearch(0, [1, 3]) -> -1
bsearch(2, [1, 3]) -> -1
bsearch(4, [1, 3]) -> -1

Siguiente prueba:

if __name__ == '__main__':
   test_bsearch(5)

Falla:

...
bsearch(1, [1, 3, 5, 7]) -> 0
bsearch(3, [1, 3, 5, 7]) -> 1
bsearch(5, [1, 3, 5, 7]) -> -1
AssertionError: 2 was expected, but got -1

Bien, ahora el resultado no está ni al principio, ni al final ni tampoco en el medio (recordemos que estamos usando aritmética entera, por lo que (0 + 3) / 2 = 1).

Eso significa que debemos examinar los elementos intermedios. Pero recordemos que si procedemos a evaluarlos uno por uno como lo hemos venido haciendo, la búsqueda se convierte en una operación con un tiempo de ejecución de O(n). Por ello, solo hemos de examinar la mitad con probabilidad de contener el elemento buscado:

def bsearch(what, seq):
   start, end = 0, len(seq) - 1

   while start <= end:
       half = (start + end) / 2
       if seq[half] == what:
           return half
       if seq[half] < what:
           start = half + 1
       else:
           end = half - 1
   return -1

Todas las pruebas pasan:

...
test_bsearch(14)
...
test_bsearch(19)
...
test_bsearch(25)

¿Qué pasa si en lugar de listas de números impares usaramos listas de números pares?

if __name__ == '__main__':
   test_bsearch(5)
   pares, impares = impares, pares
   test_bsearch(5)

Desarrollo

Como los músicos harían, una vez que tenemos una técnica bajo nuestro comando, habrá que buscar todas las variaciones posibles.

¿Podemos escribir una versión recursiva en lugar de iterativa?, ¿Usar multiples threads?, ¿En cuantos lenguajes podemos desarrollar nuestra kata, logrando que nuestra implementación no solo sea correcta, sino también idiomática del lenguaje seleccionado?

Saludos

Apéndice

A continuación, incluyo el código de las funciones trace y assert_equal, usadas en el desarrollo del kata.

class ArgFormatter(object):

   def __init__(self, args, kwds):
       (self.args, self.kwds) = (args, kwds)
       self.params = []

   def _clear(self):
       self.params = []

   def _addPositional(self):
       self.params.extend(repr(arg) for arg in self.args)

   def _addKeywords(self):
       self.params.extend('%s=%r' % (k, valor) for (k, valor) in self.kwds.iteritems())

   def _assemble(self):
       return ', '.join(self.params)

   def Format(self):
       self._clear()
       self._addPositional()
       self._addKeywords()
       return self._assemble()


def trace(f):
   from functools import wraps

   def formatParams(args, kwds):
       return ArgFormatter(args, kwds).Format()

   @wraps(f)
   def decorator(*args, **kwds):
       result = None
       params = formatParams(args, kwds)
       print '%s(%s) ->' % (f.__name__, params),
       result = f(*args, **kwds)
       print repr(result)
       return result
   return decorator


def assert_equal(expected, actual):
   message = "%s was expected, but got %s" % (expected, actual)
   assert expected == actual, message

28 de mayo de 2011

Interfaz de línea de comandos (CLI) y separación de ocupaciones.

Hace no mucho, me dí a la tarea de comenzar a crear una interfaz de línea de comandos (CLI por sus siglas en inglés) para el sistema al que actualmente estoy dando mantenimiento, además de ser un ejercicio intelectual bastante provechoso, obtuve resultados muy satisfactorios en cuanto al objetivo de lograr una separación del código de interfaz de usuario del código de dominio, el cual es uno de los problemas más críticos (y crónicos) que tiene el sistema en cuestión.


Como sabemos, debido a la filosofía subyacente en la gran mayoría de los entornos gráficos de programación actualmente en uso, los famosos IDE-RAD's (Rapid Application Development Environments), primero se diseña la interfaz de usuario y luego como no hay ningún lugar para poner la lógica del dominio, como no sea en la base de datos (stored procedures), si no se tiene cuidado, éste código acaba por desgracia casi siempre horriblemente mezclado y acoplado con el código de la interfaz de usuario. La base de código fuente así creada, se torna difícilmente mantenible y con el paso del tiempo y al ir cambiando los requerimientos se convierte en un serio problema en el mejor de los casos y la mayoría de las veces termina como una gran "bola de lodo" (del antipatrón big ball of mud).


Ya desde finales de los años 70's del siglo pasado, Smalltalk-80 introducía el paradigma (patrón de diseño propiamente dicho) MVC (Model-View-Controller) para evitar los problemas antes mencionados. Ya tendremos ocasión de discutir sobre el mencionado patrón de diseño posteriormente.


La cuestión es que muchos de nosotros no nos damos cuenta del problema, lo que es peor, ni siquiera lo vemos como tal. En mi caso fué hasta después de muchos años de recurrentes problemas de mantenimiento de código y de encontrarme una y otra vez sumergido en pantanos de código así mezclado (sin mencionar el sistema al que actualmente doy mantenimiento), que definitivamente supe que algo no estaba bien, mas aún: que algo estaba muy, muy mal.


Al principio no sabía exactamente que éra. Andando el tiempo y después de investigar, toparme con la verdadera POO y los patrones de diseño, sufrir un cambio de paradigma (paradigm-shift) y sobre todo, después de conocer, reflexionar e investigar mas a fondo algunos principios fundamentales de la programación tales como la modularidad, la no-redundancia, la claridad, la simplicidad, la alta cohesión, el bajo acoplamiento, la verificabilidad, entre otros, fué que me dí cuenta de la situación real.


Me dí cuenta que la razón, causa y origen de mis problemas era precisamente el estar transgrediendo sistemáticamente un principio fundamental que tiene su origen y aplicación en todas las areas de la vida en general y no solamente en la programación de sistemas informáticos. Me refiero al principio de separación de ocupaciones (y/o preocupaciones), mejor conocido en inglés como separation of concerns.


El origen del problema era que estaba yo mezclando a la fuerza y en un mismo recipiente dos cosas que son totalmente distintas e independientes una de la otra ("ortogonales" dirían algunos). Esto es: la interfaz de usuario y la lógica del dominio.


A continuación quiero compartir con ustedes algunas citas memorables de Martin Fowler sobre el tema.


"Domain objects should be completely self contained and work without reference to the presentation, they should also be able to support multiple presentations, possibly simultaneously. This approach was also an important part of the Unix culture, and continues today allowing many applications to be manipulated through both a graphical and command-line interface." - http://martinfowler.com/eaaDev/uiArchs.html


"So, if you write an application with a WIMP (windows, icons, mouse, and pointer) GUI, you should be able to write a command line interface that does everything that you can do through the WIMP interface—without copying any code from the WIMP into the command line." - http://www.martinfowler.com/ieeeSoftware/separation.pdf


"A good mental test to use to check you are using Separated Presentation is to imagine a completely different user interface. If you are writing a GUI imagine writing a command line interface for the same application. Ask yourself if anything would be duplicated between the GUI and command line presentation code - if it is then it's a good candidate for moving to the domain." - http://martinfowler.com/eaaDev/SeparatedPresentation.html


Así pues, al verme enfrentado por enésima ocasión al mismo problema del alto acoplamiento (o mezcla completa) de la interfaz de usuario con la lógica del dominio. Decidí llevar a la práctica los consejos de Martin Fowler y me puse a escribir una interfaz de línea de comandos para el módulo del sistema al cual estaba dando mantenimiento en ese momento. Al hacerlo obtuve una visión sumamente clara de los objetos de dominio necesarios así como de dónde y de qué manera necesitaba refactorizar el código. Asimismo, obtuve código mas limpio, objetos mejor factorizados, separación completa de la interfaz de usuario en el módulo en desarrollo y además una nueva forma de probar los objetos del dominio sin la interfaz de usuario. ¡Todo desde la línea de comandos!.


La experiencia anterior me ha llevado a recomendar ampliamente el ejercicio de escribir una interfaz de línea de comandos para las partes de los sistemas con interfaz de usuario gráfica (GUI) en los cuales no estemos muy seguros de cuáles sean los objetos de dominio necesarios o bien tengamos los problemas crónicos de acoplamiento anteriormente mencionados.


Esto también me hizo recordar que el proceso de creación de una aplicación GUI en Smalltalk es: 1.- Diseñar el modelo del dominio 2.- Probar el modelo del dominio 3.- Diseñar la interfaz de usuario. (http://www.object-arts.com/downloads/docs/index.html)


O dicho de otra forma: Primero lo primero (First things first). En cuenstiones de programación y en especial en POO: El dominio es primero.


Gracias por leernos, buen fin de semana y como siempre todos sus comentarios son bienvenidos.


11 de noviembre de 2010

Delegación simple con Python

Al estudiar el libro "Design Patterns: Elements of Reusable Object-Oriented Software", nos damos cuenta que frecuentemente debemos delegar métodos desde una clase "envoltorio" hacia una instancia de una clase "envuelta" (o "sujeto", para usar la terminología de los GOF). Por ejemplo los patrones decorador y proxy son dos ejemplos en los que se requiere hacer esto.

Sin embargo, una situación recurrente al usar estos patrones es que en realidad no queremos redefinir o controlar todos los métodos del sujeto. Algunas veces, solo queremos "decorar" o controlar ciertos métodos y dejar el resto en paz. En lenguajes como Java, C# o (Dios nos guarde) Visual Basic, esto implica escribir una larga lista de métodos que lo único que hacen es delegar hacia el sujeto.

Así, si tenemos una clase a la que nos gustaría decorar, pero únicamente queremos decorar un único método, en Java tendríamos algo así:

interface MySubject
{
  void decorated(char x);
  void foo();
  long bar(int blah);
  int baz(int x, float y, char z)
  ...
}

class MyDecorator implements MySubject
{
  public MyDecorator(MySubject subject)
  {
     this.subject = subject;
  }

  public void decorated(char x) {
    if (x != 'x') {
      self.addBehaviour();
    }
    this.subject.decorated();
    self.addMoreBehaviour();
  }

  private void addBehaviour() { ... }

  private void addMoreBehaviour() { ... }

  public void foo() {
    this.subject.foo();
  }

  public long bar(int blah) {
    return this.subject.bar(bla);
  }
  ....
}

etc.

En definitiva es mucho trabajo. En python es tan sencillo como definir un solo método (aunque, hay que decirlo, se trata de un método "mágico"). Dicho método es __getattr__. Este método es llamado por python cada vez que la búsqueda de un atributo en un objeto falla. Es decir, si nosotros escribimos:

foo.bar()

Pero el método bar no existe en la clase del objeto foo, entonces obtendremos una linda excepción AttributeError. Sin embargo, python nos da una oportunidad de atrapar estos "eventos" antes de levantar la excepción. Si nuestra clase define el método mágico __getattr__, python lo mandará llamar cada vez que no pueda encontrar el atributo solicitado.

Esto significa que nuestro decorador en java se simplifica significativamente al implementarlo en python:

class MyDecorator(object):
    def __init__(self, subject):
        self._subject = subject

    def decorated(self, x):
        if x != 'x':
            self._addBehaviour()
        self._subject.decorated()
        self.:addMoreBehaviour()

    def _addBehaviour(self):
        ...

    def _addMoreBehaviour(self):
        ...

    def __getattr__(self, name):
        return getattr(self._subject, name)

¡Eso es todo!

  1. En primer lugar en python no necesitamos definir una interfase ya que utiliza duck typing.

  2. En segundo lugar, con solo definir __getattr__, automáticamente estamos delegando todos los métodos que no definimos explícitamente hacia _subject,

De esta forma, si escribimos:

myobj = MyDecorator( SomeObject() )
myobj.decorated('x')

pasará exactamente lo mismo que en la versión Java. Sin embargo, si escribimos esto:

myobj = MyDecorator( SomeObject() )
print myobj.bar(25)

python tratará de invocar el método bar en myobj. Como ese método no existe en la clase MyDecorator, python seguidamente buscará el método __getattr__. En este caso, la búsqueda tiene éxito por lo que __getattr__ es invocado. Este a su vez nos regresa una referencia al método bar de la clase SomeObject y finalmente python puede llamar al método.

¿Pero, qué pasa si el método llamado tampoco existe en el sujeto? Bueno, el contrato para __getattr__ indica que de no encontrar el atributo solicitado, se debe levantar una excepción AttributeError. Y eso es exactamente lo que hace la función getattr si solicitamos un atributo que no existe, por lo tanto, cumplimos al 100% con nuestro contrato, tanto con python como con el código cliente, que ni siquiera se entera que no está hablando con una instancia de SomeObject real.

Esto es tan útil que he hecho una pequeña clase de utilería para usarla una y otra vez:

class SimpleDecorator( object ):
    u'''
    Es un "mixin" que ayuda en la implementación de patrones de diseño, al 
    permitir la delegación automática de los mensajes que el objeto no 
    entienda hacia otro objeto "subject".
    '''
    def __init__( self, subject ):
        self._subject = subject

    def __getattr__( self, name ):
        return getattr( self._subject, name )

Saludos

14 de junio de 2010

To override or not to override?... ya no es un dilema

Ok, continuando con las penurias de nuestro último proyecto en Python y sobre cómo el uso de TDD puede hacerlas más llevaderas, hoy quiero mostrar una técnica que me resulta muy útil.

Normalmente no me gustan las entradas del tipo "receta de cocina", creo que ya hay varios blogs dedicados a ese tipo de tips, sin embargo, como "blog" viene de "log" que a su vez es "bitácora", pues bueno, usaré este espacio como dicha bitácora para registrar lo que voy encontrando: lo bueno, lo malo y lo feo (que es mucho), para que después no se me olvide y, si de paso a alguien más le resulta útil, pues qué mejor.

Bueno, entrando en materia en el post anterior les platicaba yo cómo comenzar a agregar pruebas de caracterización en código legacy. Mientras me encontraba haciendo dichas pruebas, me topé con que el código en cuestión genera un archivo de .xls, mismo que contiene una página de resumen y tres páginas de detalle, donde se guardan los registros que se procesaron correctamente, los que no se pudieron procesar porque falta información y los que tuvieron errores de cálculo (imaginense nomás).

Bueno, como PyDev en realidad hace un trabajo bastante decente con el refactoring "Extract Method", me colgué de allí para encontrar cúmulos de lineas relacionadas e irlas extrayendo en rutinas. Al final terminé con una pequeña clase que escribe la información en las hojas de excel según se va procesando. Ahora lo que necesitaba era poder "detectar" esa información para poder verificar que el proceso fuera correcto.

Ya anteriormente mencioné lo facil que es crear una sub-clase en Python para propósitos de prueba, con el fin de poder redefinir ciertos métodos. También mencionamos que para los casos en los que solo necesitamos redefinir un método, existen otras alternativas.

En Python, es posible alterar la estructura de un objeto en tiempo de ejecución, por lo que facilmente podemos sustituir el método que necesitamos por otra cosa distinta. Veamos un ejemplo un poco más concreto:

Como ya tenemos una clase GeneradorTesting, me cuelgo de ahí para crear un "Factory Method" que cree nuestro libro de excel: Utilizando "Extract Method" con PyDev en la clase GeneradorArchivosobtengo:

class GeneradorArchivos( object ):
 def _creaWorkbook( self, nombre ):
   return MyWorkbook( nombre )

MyWorkbook es la clase que cree como interfaz con el código de creación del archivo .xls. Ahora, planeo que cada vez que se agregue un registro a una hoja de mi libro, agregar dicho registro a una lista en donde poder verificarlo posteriormente. Tambien quiero tener una lista separada para cada hoja. Para esto, creo un diccionario vacio en mi sub-clase de pruebas:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 def __init__( self, frame, batch_id ):
   super( GeneradorArchivosPrueba, self ).__init__( frame, batch_id )
   self.registros = {}

Bien, ahora tengo un problema: la creación de hojas individuales es una responsabilidad de MyWorkbook, no de GeneradorArchivos (que es en parte la razón por la cual creé esa clase en primer lugar). Por lo tanto, no puedo redefinir un método en GeneradorArchivosPrueba para hacer esto.

Podría crear una sub-clase de MyWorksheet llamada MyTestingWorksheet y despues redefinir el método _creaWorkbook para que regrese una sub-clase de MyWorkbook, que a su vez devuelva instancias de MyTestingWorksheet, pero creo que resulta demasiado lio para lo que yo necesito. Parece ser que estoy atorado.

Bueno, en realidad no. Gracias a algunas características muy útiles de Python, como el que pueda redefinir ciertas cosas al vuelo, puedo crear instancias "personalizadas" de las clases de "producción" y usarlas en mis pruebas. Veamos:

Lo primero que necesito es que la creación de los objetos hoja resida en su própio método:

class MyWorksheet( object ):
 ...
 def _creaHoja( self, titulo ):
   hoja = self._libro.add_worksheet( titulo )
   ...
   return hoja

Como siempre, corremos las pruebas que ya teníamos para verificar que lo demás siga funcionando. Después, redefinimos nuestro Factory Method para objetos libro:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   return workbook

Ahora, después de volver a correr las pruebas, ya que puedo controlar la creación de libros. Personalicemos dichos objetos para nuestras pruebas:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja

   def nuevaCreaHoja( *args, **kwds ):
     return old_crea_hoja( *args, **kwds )

   workbook._creaHoja = nuevaCreaHoja
   return workbook

Probamos y vemos que las cosas parecen seguir funcionando hasta aquí... ¡¡fiiuuu!!

En este punto, aún no he cambiado el comportamiento del código, sin embargo, ya estoy en posibilidad de hacerlo.

Antes de continuar, me gustaría llamar su atención a la variable old_crea_hoja. Aquí estamos guardando una referencia a la definición original del método creaHoja. Sin embargo, si miramos con cuidado, vemos que es una variable local... ¡¡se pierde al momento de salir de _creaWorkbook!! Bueno, no en realidad.

Lo que sucede aquí es que Python está creando un closure para la función nuevaCreaHoja. En términos simples, lo que esto significa es que el contexto de ejecución de nuevaCreaHoja es guardado y después recuperado cuando se llama a la función, por lo que las variables locales de la función superior a las cuales nuevaCreaHoja tiene acceso, conservan su valor cada vez que esta se ejecuta. Esto incluye a old_crea_hoja por supuesto, y esto es lo que nos permite llamar al método original.

Bueno, ahora necesitamos agregar elementos a nuestro diccionario registros cada vez que se agregue una hoja:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja

   def nuevaCreaHoja( *args, **kwds ):
     hoja = old_crea_hoja( *args, **kwds )
     self.registros[hoja.titulo] = []
     return hoja

   workbook._creaHoja = nuevaCreaHoja
   return workbook

¡Perfecto! Ahora (después de probar, claro está), podemos aplicar exactamente la misma técnica para lograr que al agregar registros a cada hoja se registren en la lista que les corresponde:

class GeneradorArchivosPrueba( GeneradorArchivos ):
 ...
 def _creaWorkbook( self, nombre ):
   workbook = GeneradorArchivos._creaWorkbook( nombre )
   old_crea_hoja = workbook._creaHoja
     def nuevaCreaHoja( *args, **kwds ):
       hoja = old_crea_hoja( *args, **kwds )
       self.registros[hoja.titulo] = []
       old_agrega_registro = hoja.agregaRegistro
       def nuevaAgregaRegistro( *args ):
           self.registros[hoja.titulo].append( args )
           return old_agrega_registro( *args )
       hoja.agregaRegistro = nuevaAgregaRegistro
       return hoja
   workbook._creaHoja = nuevaCreaHoja
   return workbook

¡Puf, casi terminamos!. Ahora todo lo que nos hace falta es verificar la información que estamos escribiendo a la hoja de cálculo en nuestra suite de pruebas. Para ello, genero la hoja correspondiente con la versión sin modificar de la aplicación y a partir de esta obtengo mi información de referencia:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
 def checaNegativos( self, *esperados ):
   for esperado in esperados:
     assert esperado in self.gen.registros["Negativos"]

 def testGenera9981( self ):
   """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
   self.gen.genera(9981)
   assert self.gen.registros_totales == 82
   assert self.gen.registros_negativos == 2
   self.checaNegativos( ('9981',1834,'GOMEZ, PACO','',516.34),
                        ('9981',2359,'DOE, JOHN','',1677.42') )

Saludos y hasta la próxima

8 de junio de 2010

Porque TDD no es opcional

Como probablemente sabrán, Alcides y un servidor hemos estado los últimos meses dándole mantenimiento a una aplicación legacy en python. Bueno, creo que es una buena oportunidad de comentar algunos de los problemas más duros con los que nos hemos topado y, como a pesar de ser problemas comunes en la mayoría de los proyectos, al estar en un lenguaje dinámico como python estos se exacerban.

Creo que el problema principal es el excesivo nivel de acoplamiento presente en la totalidad de la aplicación. El siguiente ejemplo fue extraído del código de un proceso por lotes que genera una serie de archivos TXT para realizar transferencias electrónicas masivas:

  numero_cuenta, nombre_beneficiario = self.frame.obtenDatosBanco( str( numero_empleado ) )

nota: el código ha sido modificado para no comprometer la confidencialidad del cliente y para no ofender los castos ojos del amable lector con detalles de formato, nomenclatura, estilo, etc., presentes en el original)

En el ejemplo anterior, self.frame se refiere a una referencia que guarda el objeto a la forma que lo invocó. Desde ahí uno se puede preguntar ¿Porqué un objeto que ejecuta un proceso batch requiere una referencia a la interfaz de usuario?. Más aún, ¿Porqué ese elemento de la GUI implementa un método que claramente pertenece al dominio del negocio (obtenDatosBanco)?

Al analizar el objeto en cuestión, nos dimos cuenta que las llamadas a self.frame.* se encuentran por todas partes en el código. Prácticamente no había manera de saber qué podíamos afectar si cambiábamos el batch o la ventana.

Una complicación adicional es que python, al ser un lenguaje interpretado, no genera errores de compilación u otra clase de advertencia si, por algún descuido, modificamos adversamente las dependencias del código mientras lo modificamos. Si bien es cierto que existe una herramienta (¡Muy útil!) que nos asiste en el proceso, llamada pylint, lo cierto es que aún así es necesario leer y re-leer el código varias veces tomando notas intensivamente y tratando de descubrir todos los posibles efectos que pueda tener una modificación.

Ahora, si bien en cualquier pieza de código el proceso anterior resulta extenuante, cuando se trata de un mega-archivo fuente de al rededor de 40,000 líneas de código y con un nivel extremadamente alto de acoplamiento, es poco decir que la tarea se vuelve insufrible.

Existe una técnica que nos ha dado resultado, llamada "Pruebas de Caracterización", documentada por Michael Feathers en su libro "Working Effectively with Legacy Code". Básicamente, lo que hacemos es crear un arnés de pruebas alrededor del código que deseamos modificar. La diferencia con las pruebas unitarias tradicionales es que en este caso, las pruebas no pueden ser escritas antes que el código (obvio), por lo tanto no pueden ser una especificación de lo que el código debe hacer. En cambio, las pruebas documentan lo que el código hace actualmente.

Esto significa que es probable que el código actual contenga bugs. Sin embargo, nuestro arnés de pruebas puede o no detectar dichos bugs, ya que no es esa su función. En cambio, su función es la de hacer visible el comportamiento actual del código, de tal suerte que podamos hacer las modificaciones necesarias y aún estar razonablemente seguros de que el resto aún funciona. Lo que es más, el tener nuestras Pruebas de Caracterización en su lugar, ¡No excluye el escribir pruebas unitarias para el código nuevo que se deba escribir!

Usualmente la prueba más difícil es la primera: crear una instancia del objeto en cuestión dentro de nuestro arnés. En este caso, es algo así:

from pago_electronico import GeneradorArchivos

class TestGeneradorArchivos( TestCase ):
    def setUp( self ):
        self.gen = GeneradorArchivos( frame=None, batch_id=None )
    def testCrea( self ):
        """Verifica que sea capaz de crear una instancia en el ambiente de pruebas"""
        self.assertIsNotNone ( self.gen )

Inicialmente, la prueba falla, ya que se requiere pasar una instancia del frame antes mencionado para inicializar la instancia. Sin embargo, no queremos crear una instancia de ese frame para esta prueba ya que por una parte, es una clase monstruo de arriba de 11 mil lineas de código que a su vez incluye toneladas de dependencias con ella. Por otra parte, requeriría la inicialización del sistema wxPython para el manejo de interfaz de usuario, lo que nos implicaría inicializar una instancia de wx.App... bueno, creo que ustedes captan la idea.

En su lugar, lo que queremos hacer es sustituir esa referencia a frame con otra cosa que se más fácil de crear y que podamos controlar.

Una de las propiedades de frame que utiliza GeneradorArchivos es connection, que regresa una referencia a la conexión con la base de datos. Anteriormente había desarrollado una sub-clase de TestCase para esos caso (con el plus de que esta última se asegura de abrir y cerrar la conexión cuando sea necesario, evitando la fuga de recursos). Así, lo primero que hago es cambiar TestGeneradorArchivos para ser un descendiente de mi clase:

from pago_electronico import GeneradorArchivos
from tests.custom import ConnectedDatabaseTest

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=None )

La llamada al método setUp de la superclase es necesaria para asegurarnos que la inicialización de la conexión se haga correctamente. Noten que también se pasa self como el parámetro frame.

Ahora bien, para todos los que leen esto que están acostumbrados a C#, Java, etc., dirán "eso es un error". Claramente, en esos lenguajes no podría hacer esto, ya que TestGeneradorArchivos no es de la clase adecuada que espera el constructor de GeneradorArchivos. Sin embargo, recordemos que:

Python es un lenguaje dinámico al que no le importa mucho la clase particular a la que pertenecen nuestros objetos, sino su tipo.

¿Su tipo?, ¿Que, no es lo mismo? er... no.

  • La clase de un objeto es el molde a partir del cual se crean instancias del mismo. Todo lo que tiene la clase terminará siendo parte de los objetos que a partir de ella se creen.

  • El tipo de un objeto esta definido por el conjunto de mensajes a los que dicho objeto puede responder.

En los lenguajes estáticos como Visual Basic, Java, C#, etc., la clase de un objeto define también su tipo, ya que también define los mensajes a los que ese objeto responderá. En dichos lenguajes y otros similares, clase y tipo son conceptos inseparables (o casi, si consideramos el uso de interfaces).

Por otro lado, en python podemos pasar una referencia a cualquier objeto como parámetro de una función y esta no se inmutará. Lo único que importa es que si la función llama a foo.bar(), el objeto que pasemos en el parámetro foo responda al mensaje bar.

De vuelta a nuestro ejemplo, vemos que la primera prueba pasa. Hora de escribir una segunda prueba. En este caso, las pruebas de caracterización son más como mini-pruebas de integración, ya que describen escenarios en lugar de solo verificar el resultado de una sola operación. Veamos:

    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()

Esta prueba falla inmediatamente, ya que al no especificar un batch_id, la clase lanza una caja de dialogo para obtener dicho ID del usuario. Sin embargo, como no hemos incluido wxPython en nuestra prueba, levanta una excepción.

Esto se arregla fácilmente pasando la información requerida en el constructor:

    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )

Claro, primero nos aseguramos que el batch 1234 exista y cumpla con los requisitos que necesitamos para nuestra prueba.

Ahora la prueba falla, pues está intentando acceder al método GetCurrentDate del frame. De nuevo, ¿que tiene que hacer un método como GetCurrentDate ahí, no lo sé. Sin embargo, le daremos lo que requiere. Como estamos pasando una referencia a self en el constructor, crearemos un método stub en TestGeneradorArchivos:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def GetCurrentDate( self ):
        return ( 1, 3, 2010 )

Y así sucesivamente.

Otro problema con el que nos topamos es que repetidamente GeneradorArchivos intenta crear cajas de diálogo para mandar mensajes al usuario o pedir información del mismo. Normalmente mientras estamos haciendo pruebas de caracterización, no hacemos refactorings. Esto es, porque para hacerlos correctamente, ¡Necesitamos pruebas que cubran el código que queremos refactorizar! Es como el problema de la gallina y el huevo.

Sin embargo, en este caso los refactorings son extremadamente simples y podemos realizarlos con un nivel aceptable de seguridad:

class GeneradorArchivos( object ):
    def obtenPrimerSecuencial( self ):
        return wx.GetTextFromUser( caption=u"Número Secuencial", message=u"Digite el Número Secuencial a usar", default_value="1" )

    def genera(self):
        ...
        inicial = self.obtenPrimerSecuencial()
        ...

A continuación, podemos aplicar la técnica estándar de sustitución de métodos en una prueba unitaria:

class GeneradorArchivosPrueba( GeneradorArchivos ):
    def __init__( self, frame, batch_id ):
        super( GeneradorArchivosPrueba, self ).__init__( frame, batch_id )
    def obtenPrimerSecuencial( self ):
        return "1"

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivosPrueba( frame=self, batch_id=1234 )

En este caso, hay otros métodos que queremos redefinir en GeneradorArchivos, por lo que conservaremos la clase GeneradorArchivosPruebas. Sin embargo, si solo necesitáramos redefinir obtenPrimerSecuencial o un par de métodos simples más, existe una forma más sencilla de hacerlo:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        def regresaUno(): return "1"
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )
        self.gen.GeneradorArchivosPruebas = regresaUno

o incluso:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def setUp( self ):
        super( TestGeneradorArchivos, self ).setUp()
        self.gen = GeneradorArchivos( frame=self, batch_id=1234 )
        self.gen.GeneradorArchivosPruebas = ( lambda "1" )

Ahora que ya logramos ejecutar correctamente a GeneradorArchivos en un arnés de prueba, ¿qué probamos? Uno de los métodos que extrajimos al eliminar el uso de diálogos fue muestraResumen, que simplemente muestra un resumen del trabajo realizado por el batch:

class GeneradorArchivos( object ):
    ....
    def muestraResumen( self, registros_totales, registros_negativos ):
        Mensajes.MuestraAviso( FMT_RESUMEN % ( registros_totales, registros_negativos ), parent=self.frame )

bueno, podemos utilizar este método para verificar estos resultados:

class GeneradorArchivosPrueba( GeneradorArchivos ):
    registros_totales = 0
    registros_negativos = 0
    def muestraResumen( self, registros_totales, registros_negativos ):
        self.registros_totales = registros_totales
        self.registros_negativos = registros_negativos

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()
        assert self.gen.registros_totales == -1
        assert self.gen.registros_negativos == -1

Esto nos da un primer acercamiento a las pruebas de caracterización. Al correr el proceso batch en una base de datos de prueba contra el batch número 1234, descubrimos que procesa un total de 36 registros, de los cuales 4 son negativos. Como nuestra prueba debe describir lo que el programa hace actualmente, incluimos estos resultados en nuestra prueba:

class TestGeneradorArchivos( ConnectedDatabaseTest ):
    def testGenera( self ):
        """Genera los archivos de pago y los compara contra un set de 'buenos conocidos'."""
        self.gen.genera()
        assert self.gen.registros_totales == 36
        assert self.gen.registros_negativos == 4

Este proceso continúa hasta que nos sentimos satisfechos con nuestras pruebas de caracterización, rompiendo dependencias y detectando resultados. A continuación pudimos hacer las modificaciones que se nos pidieron y de paso pudimos darle una muy necesaria limpieza al código—no tanto como hubiésemos querido, pero es un comienzo.

Saludos y espero que haya sido de su interés.