De Oráculos y MSProject

Hoy he recibido un cronograma en un archivo .mpp que no tenía una aplicación asociada para abrirlo así que busco en Google “.mpp file extension” ¡Y claro!!! Es un project!

Juro que no lo recordaba.  Creo que hace 6 o 7 años que no veo uno así que me puso nostálgico. Lo que más me gusta del project es que tiene información muy útil como que el 24 de Enero del 2012 vamos a estar todos festejando la culminación exitosa del proyecto o que el módulo XXXX va a comenzar el 10 de Octubre y que luego de 4,3 semanas el cliente nos va a dar el Ok.

¡Esta información SÍ que es valiosa!

Nota: Sí, estás en lo correcto, es una sobredosis de sarcasmo.

Cómo afectan los Mock frameworks nuestras pruebas unitarias

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.

Cuán cortas y cuan claras deben ser las pruebas unitarias?

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;

    }

}

Resultados con TDD en Microsoft, IBM, HP y Ericsson

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:

Qué significa el porcentaje de cobertura de código

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.

Como refactorizar métodos estáticos

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.

Fluent Interfaces y TDD

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.

Como fracasar con las pruebas unitarias

Hace poco gravé un pequeños video en el que explicaba una realidad que he visto en muchos proyectos respecto de las pruebas unitarias. En síntesis lo que comentaba era que en esos proyectos, los beneficios de las pruebas unitarias no eran visibles mientras que los costos sí lo eran.

En problema aparente era la calidad de las pruebas, pero en realidad, el problema de fondo es la estrategia de hacer las pruebas luego de terminado el código. Por lo general, los programadores escriben piezas de código las cuales, para probarlas, son ejecutadas manualmente varias veces mientras que con el depurador se recorren línea por línea los algoritmos. Una vez que la feature está lista, quieren escribir algunas pruebas pero apenas de empezar se dan cuenta que el código que han escrito no es fácilmente testeable, no contempla la posibilidad de inyectarle las dependencias y probablemente han utilizado muchos de lo “enemigos de las pruebas unitarias”.

Aquí el programador puede tomar un de los siguientes caminos:

  1. Refactorizar el código para volverlo testeable.
  2. Probarlo como está, es decir, si el código toma valores de una tabla de la base de datos, pueden ponerle esos valores en la tabla al iniciar la prueba.
  3. No probarlo en absoluto. Esta es (Test-Never)

Estas decisiones no son libres ya que hay ciertos condicionamientos:

  1. Queda poco tiempo. Todo lo que se pudo ahorrar en depuración ya se perdió y ahora no solo se trata de escribir las pruebas sino que hay que refactorizar algo que “ya está andando” para recién luego escribir las pruebas.
  2. Probablemente esa refactorización no sea algo tan sencillo. Es probable que haya que modificar algo más de código que solo el propio. Esto ocurre cuando hay que lidiar con los métodos estáticos y otras malas yerbas ya presentes en el proyecto.
  3. El resto del equipo ya se ha encontrado en este dilema y la decisión que han toma es un antecedente de peso en la cultura del equipo.

Cual de los caminos toma el programador depende de muchos factores pero lo malo del caso es que ninguna de las tres alternativas conduce a algo bueno. Veamos por qué:

En el primer caso, se consume mas tiempo que el que se hubiese requerido si el código se hubiese hecho testeable desde el principio mediante TDD. Es probable que el programador vea esto como una pérdida de tiempo ya que su código “funciona” pero él tiene que modificarlo para “cumplir” con algo, llámese cobertura de código, número de pruebas, etc.

En cuanto a la segunda alternativa, la de probar sin modificar el código, es sin dudas un camino para realizar pobres pruebas de integración. Solo hay que hacerlas y esperar algo más de un año para ver el daño que que hacen al proyecto, cuanto cuestan y cuán poco sirven.

La tercera opción es la más coherente con el modo de desarrollo que se ha tomado pero es sin dudas una estafa. Si se ha estimado el tiempo necesario para codificar las pruebas y si ha comprometido con el equipo ha escribir pruebas para el código propio y pero no se lo lleva a cabo, entonces hay que sincerar la situación.

No interesa que tan buen programador sea, si no se escriben la pruebas interactivamente con el código se llegará tarde o temprano a esta situación.

¿Cuantas líneas de código son 9 líneas con TDD?

En mi último post presentaba una métrica (verdaderamente muy mala) sobre mi productividad en un proyecto realizado completamente utilizando TDD de manera estricta. Esta mostraba aproximadamente 9 LOC/Hs. Al mismo tiempo, y como las pruebas y el código los escribí interactivamente, escribía 11 LOC/Hs de pruebas. Esto hace un total de 19 LOC/Hs.

Ahora bien, cada 2 o 3 pruebas el código era refactorizado para eliminar duplicaciones, del mismo modo que luego de observar un patrón común en un conjunto de pruebas, las mismas se refactorizaban también. Esto sucedió varias decenas, quizás cientos, de veces.

Entonces, cuantas líneas de código son 9 líneas de código cuando el refactoring se hace minuto a minuto?

Es probable que alguien se pregunte: ¿pero, a quien le importan las LOCs?. A mi me importan, porque he comprobado la diferencia en este aspecto entre Test-First y Test-Last (o Test-Never) y esto redunda directamente en calidad.

TDD y Yo

Hace poco comencé un nuevo desarrollo y decidí grabar algunos videos de los cuales solo publiqué los primeros tres. Sucede que el hecho de saber que alguien me estaba mirando me hacía prestar mayor atención a mis palabras que al código que debía escribir. No obstante a ello, continué grabándome para tomar el tiempo y estudiarme.

La primera parte de ese desarrollo está completado y estos son los números:

66 pruebas unitarias.
15 clases. (solo 4 centrales, el resto son datacontracts, excepciones y helpers)
238 líneas de código.
300 líneas de pruebas.
1,2605 líneas de pruebas por cada líneas de código del producto.
73 es el menor índice de mantenibilidad que obtuve. 88,9 es la media.
97,74% de cobertura total.
26 horas de programación.

9,15 LOC/hora
11 LOC(test)/hora.

Como nunca me medí mi productividad ni conozco la densidad de defectos que tiene este código ni ningún otro que haya escrito, no puedo hacer comparaciones pero sí puedo sacar algunas conclusiones y dar mis apreciaciones personales.

  • TDD te lleva a realizar muchas pruebas. Si eres riguroso y no aceptas introducir ninguna línea de código sin antes tener una prueba que la respalde, terminas escribiendo muchas pruebas.
  • Muy alta cobertura de código, el único código sin pruebas es aquel que ha surgido de las recomendaciones del analizador estático de código con respecto al número de constructores que requieren las excepciones. (No obstante, nada impide que cree pruebas para estos)
  • Más líneas de pruebas que líneas de producto. Esto lo he visto antes pero para el mix “mal código-malas pruebas” pero parece que puede darse en este caso también. Debo aclarar aquí que así como el código se fue refactorizando minuto a minuto, las pruebas también fueron refactorizadas al punto tal que no existe en el proyecto ninguna prueba de más de 9 LOCs. Incluso puede verse en las capturas que existe una jerarquía de clases para las pruebas.
  • Aún el menor índice de mantenibilidad es elevadísimo. Si no me crees, obtén las métricas de tu proyecto actual.
  • Realmente no sé si 9.15 LOC/hs es algo bueno o no. A mi entender, es un número excelente ya que , haciendo un cálculo infantil (lineal), representarían casi 1.500 LOCs/mes. Ahora que si contemplamos las pruebas como algo de valor y las agregamos daría 3224 LOCs/mes, lo que a mi parecer es sumamente bueno.

 

image 

image

image

image