August 2010 - Artículos

Desarrollo un sistema para la planificación, ejecución y seguimiento de encuestas en el que uno de los requerimientos es poder crear encuestas de manera sencilla y veloz. Además las mismas deben seguir un workflow (algo informal) de revisión. Otro dato importante es que el cliente diseña encuestas que van desde aquellas con solo algunas pocas preguntas hasta esas otras que nos tienen todo un domingo respondiendo acerca de alguna ginebra o algún nuevo centro comercial.

Para rematar debo decir que los tiempos y el presupuesto son acotadísimos. Por todo esto la creación de una interface gráfica para la confección de encuestas fue descartada de plano. No solo la UI es complicada sino que también lo son su representación en la base de datos y el modelo de objetos.

Sin más vueltas, este es un ejemplo de cómo se van a confeccionar las encuestas:

Q: '¿Cómo se siente hoy?' required single
* 'Bien' points 10
* 'mal' points -10

Q: '¿Cómo disfruta de su tiempo libre?' multiple
* 'Haciendo deportes' points 10
* 'Escuchando música' points 5
* 'Viendo televisión' points -1

Esta es la forma más natural de crear una encuesta y me simplifica la vida enormemente. De esta manera se pueden compartir las encuestas por email, compararlas, modificarlas copiando y pegando, crear unas a partir de otras ya existentes, versionarlas, imprimirlas, etc. La tabla necesaria para guardar esto tiene solo dos campos Id y Encuesta :)

image 

Este es otro caso de uso de un Domain Specific Language para facilitar el desarrollo y reemplazar a los horribles XMLs.

Una de las características más importante que debe seguir cualquier código y que es particularmente importante en las pruebas de cualquier tipo es la claridad. Una prueba debe entenderse a la primera sin demasiado esfuerzo, por eso es que debe ser breve, clara y desprovista en el mayor grado posible de elementos ‘ruidosos’.

Uno de esos factores ruidosos son los frameworks de aislamiento, los mal llamados Mocking Frameworks. Es por esto que limitar su uso a aquellas ocasiones verdaderamente justificables es algo que recomiendo. Sí, eso dije: hay que limitar el uso de frameworks de aislamiento por varias razones y entre ellas, porque ensucian las pruebas. Veamos:

Las dos pruebas siguientes son idénticas en propósito salvo que la primera utiliza un stub creado por mí mientras que la segunda utiliza Rhino Mock Isolation Framework:

[TestMethod]
public void Should_retrive_an_user_from_the_repository()
{
var userManager = new UserManagementService(new RepositoryStub());
var user = userManager.GetUserById(id: 1);

Assert.AreEqual("001 - Lucas", user.Code, "The returned Code is wrong");
}

[TestMethod]
public void Should_retrive_an_user_from_the_repository()
{
var repositoryStub = MockRepository.GenerateStub<IUserRepository>();
repositoryStub
.Expect(repository=> repository.GetUserById(id: 1))
.Return(User.CreateFromName(id: 1, name: "Lucas"));

var userManager = new UserManagementService(repositoryStub);
var user = userManager.GetUserById(id: 1);

Assert.AreEqual("001 - Lucas", user.Code, "The returned Code is wrong");
}


La diferencia es clara, a la larga la primera alternativa seguirá siempre siendo sencilla y entendible, es decir mantenible, mientras que la segunda, la que utiliza un framework de aislamiento, puede requerir más complejidad para por ejemplo, actuar en función de sus parámetros, soportar muchas llamadas, contemplar el orden de las llamadas, etc.

Aparte de esto, la primera opción es más rápida, no requiere que nos aprendamos las diferencias entre todos los tipos de Stubs y Mocks de cada frameworks y sus sintaxis particulares. ¿Quién que no conozca Rhino mocks podría configurar el stub para que en una llamada, cuando el id es 5, 6 ó 7, lance una exception?. Veamos un último ejemplo de esto:

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException)]
public void Should_check_the_bounds()
{
var userManager = new UserManagementService(new RepositoryStub());
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException)]
public void Should_check_the_bounds()
{
var repositoryStub = MockRepository.GenerateStub<IUserRepository>();
repositoryStub
.Expect(repository => repository.GetUserById(
Arg<int>.Matches(!(Is.LessThanOrEqual(0) && Is.GreaterThanOrEqual(2^16)))))
.Throw(new ArgumentOutOfRangeException("user id must be between 0 and 2^16") );

var userManager = new UserManagementService(repositoryStub);
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}


Las dos pruebas son nuevamente idénticas en propósito y resultados obtenidos pero puede verse que la segunda prueba parece más complicada, y de hecho lo es. Ahora, respecto al RepositoryStub….. Cuán complejo es esta clase?

public class RepositoryStub : IUserRepository 
{
public User GetUserById(int id)
{
if(id < 0 || id > (2^16))
throw new ArgumentOutOfRangeException("user id must be between 0 and 2^16");
return User.CreateFromName(1, "Lucas");
}
}

Entre estas dos alternativas, la de usar un frameowrk y la de escribir nuestras propias clases, existe una tercera alternativa a tener en cuenta: crear nuestros stubs/mocks utilizando delegados para poder especificarles  aquello que queremos que hagan o retornen al ser invocados. Esta es una opción intermedia en cuanto al ruido que introducen. Vamos modificar la clase RepositoryStub para demostrar cómo se hacen. Aquí la tenemos:

class RepositoryStub : IUserRepository 
{
public Func<int, User> GetUserByIdFunc;

public RepositoryStub(Func<int, User> getUserFunc )
{
GetUserByIdFunc = getUserFunc;
}

public User GetUserById(int id)
{
return GetUserByIdFunc(id);
}
}

Con este patrón podemos hacer que nuestras pruebas sean tan sencillas como si hubiesemos creado un stub específico y no configurable. Así queda nuestra prueba ahora:

[TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Should_check_the_bounds()
{
var repository = new RepositoryStub(getUserFunc: ReturnLucasOrThrowAnExceptionIfIdIsOutOfBounds);
var userManager = new UserManagementService(repository);
var user = userManager.GetUserById(id: -12);

Assert.Fail("An expected exception is missing.");
}

private User ReturnLucasOrThrowAnExceptionIfIdIsOutOfBounds(int id)
{
if(id < 0 || id > (2^16))
throw new ArgumentOutOfRangeException("user id must be between 0 and 2^16");
return User.CreateFromName(1, "Lucas");
}


Es posible crear pruebas más sencillas, más legibles, más breves, más fáciles de entender, sin dependencias externas a frameworks complicados y difíciles de aprender y con total control de lo que pasa y deja de pasar en nuestro código. Solo se necesita resistir la tentación (o la moda) de utilizar frameworks de aislamiento en donde no son absolutamente necesarios.

Las pruebas unitarias debería ser así de cortas y claras:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Losoft.Temo.Security.Authorization.Exceptions;
 
namespace Losoft.Temo.Security.Authorization.Tests
{
    [TestFixture]
    public class AdminGroupSpecs : TestsWithSecurable
    {
        [TestCase("R"), TestCase("W"), TestCase("V"), TestCase("A")]
        public void Will_Allow_All_Operation_To_Admin_Accounts(string rightToVerify)
        {           
            
            Assert.That(admin.HasRight(rightToVerify, resource));
        }
 
        [Test]
        public void Should_Be_Able_To_Grant_Rights_To_Other_Accounts()
        {
            admin.Grant(right: "A", securableResource: resource, toAccount: simpleUser);
            Assert.That(simpleUser.HasRight("A", resource));
        }
 
        [Test]
        public void Should_Be_Able_To_Revoke_Rights_To_Other_Accounts()
        {
            admin.Grant(right: "W", securableResource: resource, toAccount: simpleUser);
            admin.Revoke(right: "W", securableResource: resource, toAccount: simpleUser);
            Assert.That(!resource.HasRight(simpleUser, "W", admin));
        }
 
        [Test]
        public void Should_Not_Be_Able_To_Revoke_Administrative_Right()
        {
            admin.Grant(right: "A", securableResource: resource, toAccount: simpleUser);
            Assert.That(() => simpleUser.Revoke(right: "A", securableResource: resource),
                Throws.TypeOf<SecurityException>().With.Message.EqualTo("Unable to auto revoke administrative rights."));
            Assert.That(simpleUser.HasRight("A", resource));
        }
 
        [SetUp]
        public override void SetUp()
        {
            base.SetUp();
        }
    }
}

Un poco más:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using Losoft.Temo.Common;
using Losoft.Temo.Security.Authorization.Dependency;
 
namespace Losoft.Temo.Security.Authorization.Tests
{
    [TestFixture]
    public class LogChangesSpecs : TestsWithSecurable 
    {
        [Test]
        public void Will_Log_Fault_Attempts_To_Grant_Rights()
        {
            Assert.Throws(new AssignableToConstraint(typeof(Exception)), ()=>
            simpleUser.Grant(right: "A", securableResource: resource));
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Info: 30/03/2010 03:00:00 a.m. failed attempt by simple-user to grant [A] right to simple-user."));  
        }
 
        [Test]
        public void Will_Log_Fault_Attempts_To_Revoke_Rights()
        {
            Assert.Throws(new AssignableToConstraint(typeof(Exception)), () =>
            simpleUser.Revoke(right: "A", securableResource: resource));
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Info: 30/03/2010 03:00:00 a.m. failed attempt by simple-user to revoke [A] right to simple-user."));
        }
 
        [Test]
        public void Will_Log_Fault_Attempts_To_Query_Rights()
        {
            Assert.Throws(new AssignableToConstraint(typeof(Exception)), () =>
            simpleUser.HasRight(right: "A", securableResource: resource, toAccount: admin));
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Info: 30/03/2010 03:00:00 a.m. failed attempt by simple-user to query [A] right to admin-user."));
        }
 
        [Test]
        public void Will_Log_Grant_Rights()
        {
            admin.Grant(right: "A", securableResource: resource, toAccount: simpleUser);
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Audit: 30/03/2010 03:00:00 a.m. admin-user has granted [A] right to simple-user."));
        }
 
        [Test]
        public void Will_Log_Revoke_Rights()
        {
            admin.Revoke(right: "A", securableResource: resource, toAccount: simpleUser);
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Audit: 30/03/2010 03:00:00 a.m. admin-user has revoked [A] right to simple-user."));
        }
 
        [Test]
        public void Will_Log_Revoke_Rights()
        {
            admin.Revoke(right: "A", securableResource: resource, toAccount: simpleUser);
 
            Assert.That(logger.LoggedMessage, Is.EqualTo(
                "Audit: 30/03/2010 03:00:00 a.m. admin-user has revoked [A] right to simple-user."));
        }
 
        [SetUp]
        public override void SetUp()
        {
            base.SetUp();
 
            logger = new FakeLogger();
            LoggerDependency.Instance = () => logger;
 
            DateTimeDependency.Now = () => new DateTime(2010, 3, 30);
        }
 
        private FakeLogger logger;
    }
}

Las recientes experiencias en la industria confirman que para obtener mejoras sustanciales mediante pruebas unitarias es necesario incorporar TDD (Test-Driven Development) como práctica integral del desarrollo. Aunque TDD no es una práctica nueva, solo experiencias recientes en Microsoft, IBM, HP y Ericsson comprueban la efectividad y factibilidad de ésta en proyectos reales y de reputación mundial.

Los números de los resultados en realmente asombrosos. Les recomiendo leer las siguientes publicaciones para quienes quieran conocer de los resultados obtenidos:

Esto es elemental pero nunca sobra un poco de repetición: la cobertura de código es una métrica INVERSA. Vamos a ver por qué. En la clase de abajo tenemos un solo método que probar: el ToString(). Queremos que nos devuelva el nombre completo del cliente cada vez que se lo invoque.

public class Customer
{
    private string firstName, middleName, lastName;
 
    public Customer(string firstName, string middleName="", string lastName="") 
    {
        this.firstName = firstName;
        this.middleName = middleName;
        this.lastName = lastName;
    }
 
    public override string  ToString()
    {
        string fullName = string.Empty;
 
        if (!string.IsNullOrWhiteSpace(firstName))
        {
            fullName += firstName + " ";
        }
        if (!string.IsNullOrWhiteSpace(middleName))
        {
            fullName += middleName;
        }
        if (!string.IsNullOrWhiteSpace(lastName))
        {
            fullName += " " + lastName;
        }
 
        return fullName;
    }
}

Con la siguiente prueba obtenemos el 100% de cobertura de código:

[Test]
public void ToString_must_return_the_full_name()
{
    var customer = new Customer(
        firstName: "Lucas",
        middleName: "Eugenio",
        lastName: "Ontivero");
 
    Assert.That(customer.ToString(), Is.EqualTo("Lucas Eugenio Ontivero"));
}

Pero si volvemos al método ToString() podremos a simple vista notar esos extraños espacios en blanco concatenados inmediatamente después de firstName y antes de lastName. El método obviamente está mal y lo podríamos descubrir con la siguiente prueba:

[Test]
public void ToString_must_return_the_full_name_even_with_no_middle_name()
{
    var customer = new Customer(
        firstName: "Lucas",
        lastName: "Ontivero");
 
    Assert.That(customer.ToString(), Is.EqualTo("Lucas Ontivero"));
}

Test 'ConsoleApplication3.CustomerTests.ToString_must_return_the_full_name_even_with_no_middle_name' failed:
  Expected string length 14 but was 15. Strings differ at index 6.
  Expected: "Lucas Ontivero"
  But was:  "Lucas  Ontivero"
  -------------------^

Entonces ¿para qué nos sirve el valor de cobertura de código?. Bien, si obtenemos una cobertura de código muy baja, como podría ser una por debajo del 30%, esto nos indica que deberíamos redoblar el esfuerzo para crear más pruebas ya que solo una muy pequeña porción del mismo está siendo validada. Por otro lado, un valor alto de cobertura, digamos mayor a un 70%, no nos dice absolutamente nada en el sentido de que no podemos realmente conocer cuantos “caminos” están siendo alcanzados.

En mi entrada Fluent Interfaces y TDD presentaba una prueba de concepto sobre un DSL interno que estaba desarrollando para encapsular varios detalles de la manipulación de documentos en el proyecto en el que trabajo actualmente. Luego de avanzar un tanto me doy con un problema muy común en la mayoría de las interfaces fluidas que he visto, a este patrón lo llamo sentencia única. Esto significa que una sentencia de un dsl interno no interactúa con otras del mismo. Veamos un ejemplo:

Dh.Using.AnimalControl
    .Where(
        ac => ac["Created"].LessThan(createdOn),
        ac => ac["Sex"].Not.EqualTo("Unknown"))
    .Perform(ac => ac.Document.ArchivedDate = DateTime.Now)
    .Archive();

¿Qué sucede si quisiera que los documentos fuesen accedidos por un orden dado? ¿Qué sucede si solo quiero que los documentos se archiven cuando se cumple una condición particular? ¿Qué sucede si quisiera imprimir estos documentos?. Bien, para hacer alguna de estas cosas podría extender la sintaxis así:

Dh.Using.AnimalControl
    .Where(
        ac => ac["Created"].LessThan(createdOn),
        ac => ac["Sex"].Not.EqualTo("Unknown"))
    .OrderBy(
        ac => ac["Created"].Ascendent)
    .Print();

Uhmm… el order by y el print son ahora soportados pero al costo de haber extendido la sintaxis, esto no puede estar bien. El servicio de indexado de documentos que se consulta nos permite traer páginas y definir el tamaño de las mismas para no traer un xml con 1.000.000 de resultados. ¿Cómo hago si quiero tener control de esto? ¿Debería renunciar a esta posibilidad en pos de la legibilidad y mantenibilidad?. No, lo que hay que hacer es salir del esquema totalmente declarativo. Veamos:

var ids = Dh.Query
    .Page(size: 40)
    .Where(
        ac => ac["Created"].LessThan(createdOn),
        ac => ac["Sex"].Not.EqualTo("Unknown"))
    .OrderBy(
        ac => ac["Age"].Ascendent
    );
 
Dh.Using.Documents.WithId(ids)
    .Perform(ac => { ac.Document.ArchivedDate = DateTime.Now; ac.Save(); })
        .If(ac => ac.IsMaster)
    .Perform(ac => ac.Print())
        .If(ac => !ac.IsMaster);

Esta separación nos da un fino manejo de la consulta y de las acciones a ejecutar sobre cada uno de los documentos sin complicar la sintnxis de la sentencia Using, por el contrario, ahora las expresiones soportadas son menos por lo que el lenguaje es más sencillo.

Hoy todos reconocemos el potencial que tiene (LOP) Language Oriented Programming, pero no solo eso sino que muchos ya están invirtiendo para hacerse con las ventajas prometidas por este paradigma(?). Muchos incluso diseñan la sintaxis de aquellos lenguajes que entienden, pueden hacerles alcanzar la productividad, calidad y mantenibilidad que buscan.

Ahora bien, una vez decididos a crear el lenguaje (textual) propio para un dominio particular, hay que implementarlo. Aquí es donde deben estudiarse seriamente las alternativas disponibles para alcanzar la mejor solución dado las restricciones que tenemos (tiempo, presupuesto, conocimientos de ingeniería, etc)

Antes de continuar, quiero aclarar mi definición de DSL: lenguaje pequeño para solucionar un conjunto de problemas de un dominio particular. Esta aclaración es necesaria porque con ella dejamos afuera lenguaje  como Transact SQL el cual si bien es específico, no es para nada pequeño.

Lo que debemos considerar

DSL interno o DSL externo

Este puede ser un muy buen punto de partida. ¿Queremos un DSL externo o uno interno? Antes de que veamos cómo podemos respondernos esta pregunta debemos entender lo que es un Business Natural Language (BNL). Un BNL es un DSL externo tan cercano al lenguaje natural del negocio que manejan los clientes que ellos mismos, con solo un poco de entrenamiento y soporte, son capaces utilizarlos para especificar los requerimientos y brindárselos al sistema el cual los interpreta y ejecuta. La pregunta ahora sería: ¿es esto lo que queremos, un BNL? ¿Debería un cliente poder utilizarlo?

Si es esto lo que buscamos necesitaremos con alta probabilidad un DSL externo. Un ejemplo de un BNL puede encontrarse en mi entrada [DSL] Domain Specific Languages - Un ejemplo. Por el contrario, si lo que buscamos es un medio de elevar el nivel de abstracción en el que estamos programando, un medio por el cual ser más productivos en nuestro lenguaje habitual, probablemente un DSL interno es la solución más conveniente y económica. Un ejemplo de un DSL interno (o Fluent Interface) puede encontrarse en mi entrada Fluent Interfaces y TDD.

Otras señales importantes a tener en cuenta a la hora de decidir si necesitamos un interno o uno externo es la complejidad de las estructuras. Un DSL externo no tiene ninguna restricción mientras que uno interno está limitado a las estructuras del lenguaje que manejamos. Los DSL internos mantienen el ruido sintáctico del lenguaje huésped mientras que los externos no tienen este inconveniente (más allá que cierto ruido hace al parser más sencillo y nos evita el tener un análisis semántico complejo).

¿Declarativo o Imperativo?. Esta es otra clave. Si el lenguaje es si es declarativo no importa demasiado la elección mientras que si el lenguaje es imperativo la opción a elegir debe ser el DSL externo.

Xml, parser artesanal, expresiones regulares o generador de parsers

Este punto también es clave. ¿Podemos conformarnos con especificar el comportamiento mediante el uso de un XML?. Si bien usar xml es algo espantosamente aberrante, muchas veces es la opción más sencilla y económica. Lo único a intentar es lograr un esquema lo menos ruidoso posible.

<listaDePersonas>
     <persona>Lucas</persona>
     <persona>Pablo</persona>
     <persona>Noelia</persona>
     <persona>Santiago</persona>
<listaDePersonas>

Otra alternativa es hacer un parser “a lo tonto” mediante splits por líneas y caracteres especialmente ubicados para servir de delimitadores. Por ejemplo, es muy sencillo hacer un parser rudimentario para obtener la lista de personas del código siguiente:

ListaDePersonas
      Lucas
      Pablo
      Noelia
      Santiago
FinListaDePersonas

Tampoco hay que descartar las expresiones regulares mediante RegEx para parsear un string. Como ejemplo de un lenguaje en el que su creador no sabía nada de nada de creación de parsers y que sin embargo logró su cometido con cierta elegancia pueden ver el código del parser del lenguaje E# del proyecto NBusiness en codeplex. Esto demuestra que es posible hacerlo con expresiones regulares a lo bruto.

Por último tenemos que analizar los generadores de parsers. Existen muchísimos y cada uno, como siempre, tiene sus pros y sus contras. Esta alternativa es mi preferida pero la recomendaría solo para casos en que realmente se justifique su uso. Esto podría ser cuando la complejidad y tamaño de la sintaxis del lenguaje es lo suficientemente grande y se necesita un buen control de errores y recuperación, etc.

Suena tonto pero estudiar Antlr, por ejemplo, es un tanto arduo y requiere mucha práctica y eso es solo para tener un parser, luego falta todo el resto.

Generador de código (fuente/binario), interprete o traductor

Está bien, ya tenemos resuelto lo de arriba entonces una vez que tenemos nuestro lenguaje y nuestro Modelo semántico (DSL interno) o el AST + SymTable (DSL externo) debemos decidir entre estas tres estraetgias. Si estamos hablando de un DSL interno seguramente será ejecutado por un componente del mismo sistema pero tampoco nada impide que genere código en el mismo lenguaje o en otro.

Por otra parte, si es externo, hay que ver como se lo va a usar. Si por ejemplo se utiliza embebido lo más probable es que se lo interprete como por ejemplo podemos ver abajo:

var personas = LangInterpreter.Parse(
     "Lucas\n" +
     "Pablo\n" +
     "Noelia\n" +
     "Santiago");

La generación de código fuente, si bien es horrible, tiene algunas ventajas como lo son las posibilidad de retirar el lenguaje y dejarlo sin mantenimiento sin afectar el proyecto ya que no mantenemos código en otro lenguaje sino que lo tenemos en nuestro lenguaje de trabajo obtenido de la generación a partir del código del DSL. El código generado por el generador de código suele ser preferible en formato binario salvo las excepciones mencionadas arriba.

La traducción no es común cuando hablamos de DSLs, en realidad no tiene sentido.

Costos y Beneficios

Muchos quieren subirse a este caballo sin analizar sus costos y sus beneficios. Esto mismo nos puede dar un nivel de riesgo para la creación de un lenguaje nuevo. Podemos decir que un DSL externo, de cierta complejidad, imperativo, desarrollado con Antlr y que genere código a partir de plantillas es caro, caro comparado con un DSL interno, sencillo, declarativo, desarrollado con el mismo lenguaje que se está usando y que por lo tanto es ejecutado como una sentencia más de código.

Hay que considerar que muchos desarrolladores, sino la mayoría, nunca han desarrollado un lenguaje y que muchos de ellos aprobaron la materia Compiladores sin entender realmente la diferencia entre los parsers LL y LR. Por esta misma razón hay que tener en cuenta que los lenguajes deben ser mantenidos y que encontrar desarrolladores capaces de hacerlo no es tarea sencilla.

Espero que esto ayude a pensar a quienes están pensando en crear un DSL sobre las distintas alternativas que tienen a su disposición.

Imagina que encontramos un clase estática con varios métodos estáticos los cuales tienen una cantidad aberrante de parámetros. Queremos eliminarla pero nos damos con que está siendo usada en muchísimas partes ¿que hacemos? ¿Como lo harias vos?.

Para hablar más concretamente veamos uno de esos métodos:

public static void CreateActivityLog(string containerSourceId, string containerId, 
    string action, string sourceId, string instanceId, string docNo, string notes, 
    IFrameworkSecurityContext credentials, BaseSecurityDataContract secContext, 
    string setAction)

Bien, este es quizás uno de los peores escenarios posibles porque para refactorizar necesitamos, antes que nada, contar con un conjunto de test unitarios para asegurarnos de no romper nada. Pero hay un problema con eso, ¿sabes cual es?.

El problema es que los test son dependientes de la firma del método que es precisamente lo eliminar de la faz de la tierra por lo que no nos interesa el refactorizarlo, solo eliminarlo. Además, el método podría (y de hecho seguramente es así) tener dependencias externas como accesos a bases de datos o servicios web etc. y no podemos inyectarle esas dependencias sin agregar más parámetros a la lista.

Pero entonces ¿necesitamos tests o no?. Claro que los necesitamos no son opcionales. Veamos los pasos que necesitamos para hacer lo que queremos:

1.- Crear una clase. En este caso la llamaremos ActivityLog.

// Esta es la clase testeable (proxy class) <<----------------------
public class ActivityLog
{
    private IFrameworkSecurityContext invoker;
    private BaseSecurityDataContract securityContext;
    private DocumentIdentifier container;
    private DocumentIdentifier document;
 
    public ActivityLog(IFrameworkSecurityContext invoker, 
        BaseSecurityDataContract securityContext,
        DocumentIdentifier container, DocumentIdentifier document)
        // acá es donde inyectamos las dependencias <<-----------------------
    {
        this.invoker = invoker;
        this.securityContext = securityContext;
        this.container = container;
        this.document = document;
    }
 
    // Esta es una mejor firma que nos va apermitir refactorizar <<----------
    public void CreateNewLogEntry(
        string documentNumber, string action, string notes, string setAction)
    {
        // llamamos al enjendro mutante
        ManifestHelper.CreateActivityLogDocument(
            container.SourceId.ToString(), container.InstanceId.ToString(), action,
            document.SourceId.ToString(), document.InstanceId.ToString(), 
            documentNumber, notes, invoker, securityContext, setAction);
 
    }
}

2.- Una vez que tenemos esta clase creamos el conjunto de pruebas que necesitamos. En este punto estaremos aún probando el viejo código. Eso es precisamente lo que queremos. Es más, ahora si podríamos inyectarle las dependencias agregándole más parámetros.

3.- Reemplazamos todas las llamadas al método estático con llamadas a este nuevo método. Aún estaremos utilizando el viejo código.

4.- Refactorizar el código del nuevo método creado y eliminar el viejo y mal oliente método estático.

Espero que le sirva a alguien.

Desarrollar con TDD al principio no es nada fácil pero luego se vuelve “la manera” de desarrollar. Ahora, no siempre hago TDD, si quiero probar algo tan solo tiro las lineas y listo pero, por otro lado, si quiero hacer algo bien por más que tenga algo de código hecho lo tiro y lo comienzo de nuevo con TDD (nunca he perdido mucho. Por el contrario, lo hago porque veo una diferencia).

El asunto es que diseñé una interface fluida para encapsular todos los detalles indeseables de la manipulación de documentos en el proyecto en el que trabajo actualmente. Veamos como se ve una sentencia:

var createdOn = new DateTime(2005, 1, 1);
 
Dh.Using.AnimalControl
    .Where(
        ac => ac["Created"].LessThan(createdOn),
        ac => ac["Sex"].Not.EqualTo("Unknown") )
    .Do( ac => ac.Document.ArchivedDate = DateTime.Now )
    .Archive();

Esta simple “sentencia” hace lo siguiente: invoca al servicio de consultas para solicitar los Ids de todos los documentos del módulo “Animal Control” creados con anterioridad al 1/1/2005 y con sexo conocido, luego, por cada uno de los Ids, utilizando el servicio de documentos, obtiene uno por uno de ellos (son documentos XML) y setea el elemento /Document/ArchivedDate con la fecha actual y, luego de verificar si el usuario tiene privilegios para editar,  lo “archiva” en otro repositorio utilizando el mismo servicio de documentos.

Bien, una vez terminada la prueba de concepto que asegurase que todas, o la mayoría, de las operaciones se iban a poder expresar utilizando este nuevo “DSL interno”, me decido a tirarlo y comenzarlo seriamente con TDD. Así que aprovecho para mostrar esto que suele entenderse mal. Veamos como NO se prueban:

[Test]
public void It_should_allow_us_manipulate_set_of_documents()
{
    var createdOn = new DateTime(2005, 1, 1);
 
    Dh.Using.AnimalControl
        .Where(
            ac => ac["Created"].LessThan(createdOn),
            ac => ac["Sex"].Not.EqualTo("Unknown") )
        .Perform( ac => ac.Document.ArchivedDate = DateTime.Now )
        .Archive();
 
    Assert.That("?", Is.EqualTo("?"), "?");
}

¿Cómo se escribe el Assert?  ¿Cómo le inyecto las dependencias? ¿Qué es lo que debo probar? ¿Hay algo mal aquí?. Para responder estas preguntas, debemos entender como se compone una interface fluida:

  1. El Expression Builder, como la llama Martin Fowler, es el que hace las veces de parser (en el sentido de que guia la sintaxis del lenguaje) y por lo general se compone de un conjunto de interfaces, diseñadas para posibilitarle al motor de intellisense que asista al programador en la escritura de las sentencia y, al menos una clase que implementa esas interfaces, que colecta la información de los “terminales”.  Esta o estas clases, aparte de especificar la sintaxis de la interface, son las encargadas de ir colectando la información sobre  “lo que queremos hacer”. Estas clases no tienen comportamiento significativo, son básicamente estructuras de datos con métodos y propiedades que almacenan valores y que por lo general (obviamente no siempre) retornan this.
  2. El Semantic Model, también de Fowler, es la estructura de datos en la que se almacena esa información sobre lo que “queremos hacer”, recolectada por el Expression Builder. Esta es la principal entrada para el intérprete o generador de código.
  3. El ejecutor. En realidad, en el contexto de una interface fluida (o DSL interno) no hablamos de generadores de código ni intérpretes, en su lugar tendremos un componente que ejecuta las acciones necesarias para complacernos. Este es el “ejecutor”, el que hace las cosas, el que tiene dependencias, el que puede requerir fakes, el que debe implementar la lógica real.

Visto así, podemos decir que el Expression Builder no es sino una amistosa y sintácticamente conveniente manera de completar, de llenar el modelo semántico (Semantic Model) el cual es la entrada principal para el “ejecutor”.

Entonces… ¿como se escriben las pruebas unitarias de una interface fluida?. La respuesta ahora es obvia: se prueban sus componentes (o unidades) por separado. Aquí también podemos hacerlo de manera top-down o bien bottom-up dependiendo si nos sentimos más cómodos creando primero las especificaciones del parser (Expression Builder) o bien del Ejecutor. Personalmente prefiero pensar la sintaxis primero. El pensar la sintaxis primero ayuda a conocer cuales van a ser los requerimientos los cuales no tienen detalles importantes a definir.

Martin Fowler le puso nombre a otra cosa que todos conocemos bien desde siempre pero que nunca se nos hubiese cruzado por la cabeza el bautizar, esto es: el ruido sintáctico. Bien, una interface fluida debe buscar el menor ruido sintáctico posible! por eso la propiedad “Using”  es global a Dh, para no tener que recurrir al keyword new, por la misma razón (para no introducir ruido) las dependencias no están presentes en la sintaxis. Estos parecen detalles pero el diablo está en ellos. Este es el esqueleto (incompleto) del expression builder de este dsl:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ProofOfConcept;
 
namespace DocumentLanguage
{
    public class Dh : IModules, IActionable, ISelector 
    {
        private DhSemanticModel semanticModel;
 
        private Guid lastModule;
 
        private Dh()
        {
            semanticModel = new DhSemanticModel();
        }
 
        public static IModules Using
        {
            get
            {
                return new Dh();
            }
        }
 
        public ISelector CallForService
        {
            get
            {
                this.lastModule = WellKnownModules.CallForServiceModuleId;
                return this;
            }
        }
 
        public ISelector CaseFolder
        {
            get
            {
                this.lastModule = WellKnownModules.CaseFolderModuleId;
                return this;
            }
        }
 
        public ISelector CaseReport
        {
            get
            {
                this.lastModule = WellKnownModules.CaseReportModuleId; 
                return this;
            }
        }
 
        public ISelector AnimalControl
        {
            get
            {
                this.lastModule = WellKnownModules.AnimalControlModuleId;
                return this;
            }
        }
 
        public IActionable Perform(Action<dynamic> action)
        {
            semanticModel.RegisterPerformAction(action);
            return this;
        }
 
        public IActionable Id(params Guid[] numbers)
        {
            semanticModel.RegisterDocumentIdentities(this.lastModule, numbers);
            return this;
        }
 
        public IActionable Where(params Func<Fieldx, Comparison>[] condition)
        {
            var fx = new Fieldx();
            semanticModel.RegisterConditions(
                condition.ToList().ConvertAll(f => f(fx)));
            return this;
        }
 
        public void Save()
        {
            semanticModel.RegisterCommand("Save");
        }
 
        public void Seal()
        {
            semanticModel.RegisterCommand("Seal");
        }
 
        public void Archive()
        {
            semanticModel.RegisterCommand("Archive");
        }
 
        public void Delete()
        {
            semanticModel.RegisterCommand("Delete");
        }
 
        public IModules And
        {
            get 
            { 
                return this; 
            }
        }
 
        public IActionable DocumentExtension(string extensionName)
        {
            semanticModel.RegisterDocumentExtension(this.lastModule, extensionName);
            return this;
        }
    }
 
    public interface IModules
    {
        ISelector CallForService { get; }
        ISelector CaseReport { get; }
        ISelector CaseFolder { get; }
        ISelector AnimalControl { get; }
    }
 
    public interface ISelector
    {
        IActionable Id(params Guid[] numbers);
        IActionable Where(params Func<Fieldx, Comparison>[] condition);
        IActionable DocumentExtension(string extensionName);
    }
 
    public interface IActionable
    {
        IActionable Perform(Action<dynamic> action);
        void Save();
        void Seal();
        void Delete();
        void Archive();
        IModules And { get; }
    }
 
    public class Fieldx
    {
        public Field this[string fieldName]
        {
            get
            {
                return new Field(fieldName);
            }
        }
    }
 
    public class Field
    {
        private string name;
 
        public Field(string name)
        {
            this.name = name;
        }
 
        public Comparison LessThan (DateTime date)
        {
            return new Comparison();
        }
 
        public NegativeField Not
        {
            get
            {
                return new NegativeField(this);
            }
        }
    }
 
    public class NegativeField
    {
        private Field field;
 
        public NegativeField(Field field)
        {
            this.field = field;
        }
 
        public Comparison EqualTo(object value)
        {
            return new Comparison();
        }
    }
 
    public class Comparison
    {
        internal Comparison()
        {
        }
    }
}

El modelo semántico es igualmente sencillo.

Hace aproximadamente 15 años que vengo escuchando gente repetir esta estupidez como una verdad revelada. No sé si quienes lo dicen realmente lo piensan o solo lo repiten, aunque creo que a quienes hacen copy/paste desde la cabeza de otro sin ningún filtro, poco se les puede pedir.

El código no es difícil de leer per se. Si hay código difícil de leer es porque quien lo escribió no sabía como escribirlo de una mejor manera. Así las cosas, el código es más difícil de escribir que de leer ya que cualquiera con algo de esfuerzo y tiempo puede entender una pieza de mal código pero no todos pueden tomarla y, no ya reescribirla correctamente sino tan solo mejorarla un poco. ¿Se entiende?

Si las recetas de los médicos son más difíciles de leer que de escribir es tan solo porque estos profesionales le dedican el suficiente empeño en hacer su peor letra (o bien porque son unos tarados) pero a nadie se le ocurriría afirmar que “las recetas médicas son más fáciles de escribir que de leer”.

Veamos, este es un método cualquiera que asigna algún permiso a una cuenta dada:

internal void Grant(Account toAccount, string right, Account authorizedBy)
{
    try
    {
        VerifyThatAuthorizerHasRightForOperation(authorizedBy);
        GrantTrusted(toAccount, right);
        LogChanges("granted", toAccount, right, authorizedBy);
    }
    catch (UnathorizedOperationException ue)
    {
        Logger.Info(
            string.Format("failed attempt by {0} to grant [{1}] right to {2}.",
                ue.Authorizer.Name, right, toAccount.Name));
        throw;
    }
    catch (NotPersistedException npe)
    {
        Logger.Info(
            string.Format("{0} must exist before grant a right.", ((Account)npe.Persistable).Name));
        throw;
    }
}

¿A alguien le duele el cerebro al tratar de entender que hace el método de arriba?. Yo creo que no.