Blog de Fernando Machado Piriz

Artículos sobre arquitectura corporativa y temas relacionados

Pex y Contracts: Juntos son dinamita

with 4 comments

Pex es un generador automático de pruebas de unidad que se integra con Visual Studio. Contracts es una implementación de un concepto llamado Diseño por Contrato (DBC por sus iniciales en inglés). En este artículo muestro cómo usar Pex y Contracts juntos para mejorar la calidad del código.

Hace un tiempo estuve trabajando en una aplicación en la que necesitaba cargar dinámicamente los tipos disponibles en ciertos ensamblados que implementaran cierta interfaz. No quiero entrar en los detalles cómo se resuelve ese problema de la carga dinámica de tipos (que dicho sea de paso podría haber intentado resolver con el Managed Extensibility Framework), sino cómo he usado Pex y Contracts para resolverlo.

He implementado una clase Helper que puede encontrar esos tipos buscando todos los tipos del ensamblado que implementan la interfaz dada. La clase también puede asegurarse que es posible crear instancias de esos tipos, buscando entre los métodos de instancia un constructor. Luego puede invocar el constructor para crear nuevas instancias de esos tipos.

Aquí está el código de la clase Helper para este artículo:

public class Helper
{
    public static IEnumerable<Type> LoadTypesFrom(
        string assemblyName, string interfaceName)
    {
        List<Type> result = new List<Type>();
        Assembly assembly = Assembly.LoadFrom(assemblyName);
        Type[] typesInAssembly = assembly.GetExportedTypes();
        foreach (Type type in typesInAssembly)
        {
            if (type.GetInterface(interfaceName) != null)
            {
                result.Add(type);
            }
        }
        return result;
    }

    public static IEnumerable<FileInfo> GetDllsIn(string path)
    {
        List<FileInfo> result = new List<FileInfo>();
        DirectoryInfo directoryInfo = new DirectoryInfo(path);
        foreach (FileInfo item in directoryInfo.GetFiles("*.dll"))
        {
            result.Add(item);
        }
        return result;
    }

    public static object CreateInstance(Type type)
    {
        ConstructorInfo ctor = type.GetConstructor(
        BindingFlags.Instance | BindingFlags.Public, null,
        CallingConventions.HasThis, Type.EmptyTypes, null);
        return ctor.Invoke(Type.EmptyTypes);
    }
}

El método LoadTypesFrom recibe el nombre completo incluyendo el camino de un ensamblado y el nombre de una interfaz; retorna una lista con todos los tipos en ese ensamblado que implementan esa interfaz.

La lista se retorna como IEnumerable<Type> y no como IList<Type> o ICollection<Type> para evitar que los clientes de Helper puedan siquiera accidentalmente modificar la lista quitando o agregando otros tipos que eventualmente pudieran no implementar la interfaz deseada.

El método funciona cargando el ensamblado a partir del nombre completo con Assembly.LoadFrom. Luego se obtienen todos los tipos con Assembly.GetExportedTypes. Por último se itera entre esos tipos y se determina si implementan la interfaz o no usando Type.GetInterface. Los tipos que implementan la interfaz son agregados al resultado.

El método GetDllsIn recibe el nombre de una carpeta y retorna una lista con todos los archivos con extensión .DLL en esa carpeta. La lista también se retorna como un IEnumerable<FileInfo> por las mismas razones que ya les comenté para el método anterior.

El método simplemente usa DirectoryInfo.GetFiles para obtener la lista de archivos.

El método CreateInstance recibe un tipo como parámetro y retorna una instancia de ese tipo.

El método busca primero un constructor sin parámetros usando Type.GetConstructor y luego crea la instancia invocando ese constructor con ConstructorInfo.Invoke.

Voy a crear los tests de unidad en Visual Studio, habiendo instalado Pex y Contracts previamente.

Ahora comienza la magia. Hago clic con el botón secundario en LoadTypesFrom y hago clic RunPex para invocar Pex.

image

Pex usa pruebas unitarias parametrizadas, PUT por su sigla en inglés. Una PUT es simplemente un método que toma ciertos parámetros, invoca el código que está siendo probado, y efectúa ciertas afirmaciones. Para un PUT escrito en uno de los lenguajes de .NET, Pex produce automáticamente una pequeña suite de pruebas con una alta cobertura de código. Además, cuando una prueba generada falla, Pex generalmente puede sugerir una solución. Pex realiza un análisis sistemático, aprendiendo sobre el comportamiento del programa al monitorear su ejecución, y usa mecanismo para generar nuevos casos de prueba con comportamiento diferente.

Al ejecutar Pex por primera vez, Pex encuentra una excepción ArgumentNullException y me muestra lo que ha encontrado:

image

Empezamos muy bien. Al programar Helper.LoadTypesFrom no me di cuenta que si assemblyName es null el método no va a funcionar; debería haberlo controlado. Afortunadamente Pex probó ejecutar LoadTypesFrom(null) y encontró el problema.

Típicamente resolvemos esto agregando

if (assemblyName == null) throw new ArgumentNullException("assemblyName");

al comienzo del método. Es una buena práctica de programación comprobar que los argumentos no sean null si cuando valen null el método no funciona. Pero hay dos cosas que no quedan bien resueltas aún con esta buena práctica:

  1. No puedo declarar explícitamente el prerrequisito que el método requiere que el argumento no sea null. Puedo agregarlo en la documentación y puedo también agregar en la documentación que se obtendrá una excepción ArgumentNullException si el argumento vale null. Pero no puedo evitar que un programador (ni siquiera yo mismo dentro de algún tiempo) olvide que el argumento no puede ser null; eso asumiendo que el programador leyó la documentación.
  2. No puedo lograr que el programa deje de compilar o que al menos al hacerlo me dé una advertencia si programo LoadTypeFrom(null). No sólo tengo que poder declarar los prerrequisitos del método sino que también las herramientas tienen que poder interpretar y validar esos prerrequisitos.

Aquí es donde entra Contracts.

Las raíces de este concepto se encuentran muy atrás en los fundamentos de corrección de software de C.A.R. Hoare y otros, que buscaron mecanismos para escribir programas correctos, ¡y saber que eran correctos! Más recientemente Bertrand Meyer popularizó el concepto con Eiffel.

La clave detrás del diseño por contrato es que para determinar si un programa es correcto, tenemos que tener primero una especificación de lo que el programa debe de hacer; luego es fácil: si el programa hace lo que está especificado, entonces es correcto.

No quiero aburrirlos con los detalles áridos del diseño por contrato. Basta por ahora decir que aplicado a la programación orientada a objetos, la especificación de un programa está dada por:

  1. Precondiciones. Son predicados (afirmaciones que pueden ser ciertas o falsas) en el contexto de un método que deben ser ciertos para poder invocar ese método. La responsabilidad de que el predicado sea cierto es de quién invoca el método: o hace que el predicado se cumpla o no puede invocar el método. Por el contrario, es un beneficio para el programador del método: sabe que el predicado se cumple y puede confiar en eso.
  2. Poscondiciones. Son también predicados en el contexto de un método que son ciertos cuando el método retorna. La responsabilidad de que el predicado sea cierto, es ahora del programador del método: tiene que hacer sí o sí que el predicado se cumpla al terminar el método. Por el contrario, es un beneficio para quien invoca el método: sabe que cuando el método retorne el predicado se cumple y puede confiar en eso.
  3. Invariantes. Son predicados en el contexto de una clase que son ciertos durante toda la vida de cada instancia. Las invariantes son como precondiciones y poscondiciones agregadas al comienzo y al final de cada método; y al final de los constructores.

El proyecto de Contracts de Microsoft Research no es la primera implementación de Microsoft de diseño por contrato; antes estuvo el proyecto Spec# del que tal vez alguno haya escuchado hablar.

La implementación de Contracts es brillante una vez que uno conoce todas las restricciones que tuvo que superar su equipo de diseño (antes no):

  1. No podían tocar el CLR.
  2. No podían cambiar la sintaxis de los lenguajes.
  3. No podían cambiar los compiladores.
  4. Debía estar disponible en la mayor cantidad de lenguajes posible.

Las razones para estas restricciones están, por un lado, en la cantidad de lenguajes que soporta el .NET Framework, no olviden que no son sólo C# o Visual Basic; y por otro lado, tocar el CLR puede ser fuente de inestabilidad en el .NET Framework mismo.

La solución fue:

  1. Crear un conjunto de clases para implementar precondiciones, poscondiciones e invariantes. Las mismas clases pueden ser usadas desde cualquier lenguaje.
  2. Integrarse con Visual Studio para controlar las afirmaciones en tiempo de diseño. Las violaciones de las afirmaciones aparecen en la lista de errores al compilar.
  3. Inyectar código en el momento de compilar para implementar el comportamiento asociado a las afirmaciones. Por ejemplo, las poscondiciones se programan en cualquier lugar del método, pero en el código generado están siempre al final.

La principal clase para usar diseño por contrato es Contract. En Visual Studio 2008 y .NET Framework 3.5, para usarla deben agregar el ensamblado Microsoft.Contracts.dll que está en %ProgramFiles%\Microsoft\Contracts\PublicAssemblies\v3.5 a la lista de referencias del proyecto. En Visual Studio 2010 y .NET Framework 4.0 el ensamblado está disponible por defecto. En cualquier caso, deben incluir una referencia al espacio de nombre System.Diagnostics.Contracts en la cláusula using de los fuentes en los que haya referencias a la clase Contract. Por último, tienen que habilitar el chequeo de los contratos en tiempo de diseño y de ejecución, en las propiedades del proyecto:

image

Toda esta introducción es necesaria para entender que la verdadera forma de especificar que un argumento no puede ser null es:

public static IEnumerable<Type> LoadTypesFrom(
    string assemblyName, string interfaceName)
    {
        Contract.Requires(assemblyName != null);
        Contract.Requires(interfaceName != null);
        …

Las precondiciones son declaradas con Contract.Requires. El argumento de Requires es una condición que puede ser cierta o falsa. Si en algún lugar de código hubiera una invocación Helper.LoadTypesFrom(null, null), al compilar aparecería:

image

El mensaje contracts: requires is false corresponde a la línea

Helper.LoadTypesFrom(null, null);

mientras que el mensaje + location related to previous warning corresponde a la línea

Contract.Requires(assemblyName != null);

Ahora bien, ¿cómo afecta Contracts la generación de casos de prueba con Pex?

Al ejecutar nuevamente Pex en el método LoadTypesFrom aparecen más casos de prueba y más pistas para continuar complementado la especificación del método:

image

El último mensaje de error sugiere que podemos dos agregar nuevas precondiciones a LoadTypesFrom:

Contract.Requires(assemblyName.Length > 0);
Contract.Requires(interfaceName.Length > 0);

image

El último mensaje de error sugiere nuevamente una precondición; sin embargo, la condición es más compleja que las anteriores, pues debemos especificar de alguna forma que todos y cada uno de los caracteres de la cadena son válidos. Este tipo de predicado requiere de lo que se llama un cuantificador universal: Contracts.ForAll. El método ForAll está sobrecargado, la forma en que lo vamos a usar requiere de un IEnumerable y un Predicate:

Contract.Requires(Contract.ForAll<Char>(
    Path.GetInvalidPathChars(),
    (Char c) => !assemblyName.Contains(c.ToString())));

El segundo argumento es una expresión lambda que se evalúa para cada elemento del primer argumento.

Al ejecutar nuevamente Pex podemos ver cómo el caso de prueba efectivamente ejercita la precondición agregada; y nuevamente el último mensaje de error nos da pistas sobre una nueva precondición:

image

La nueva precondición es:

Contract.Requires(assemblyName.Trim() == assemblyName);
Contract.Requires(interfaceName.Trim() == interfaceName);

Pex genera todavía más casos de prueba; y al igual que antes aparece un nuevo mensaje de error:

image

Cuando agregamos la precondición [1]

Contract.Requires(File.Exists(assemblyName));

Pex genera once casos de prueba sin ninguna excepción.

Recuerden que las precondiciones son obligaciones para quién invoque el método LoadTypesFrom. Ahora bien, ¿cuáles son las obligaciones de LoadTypesFrom? O dicho de otra forma, ¿dónde está especificado qué es lo que hace LoadTypesFrom?

Es hora de agregar poscondiciones. Las poscondiciones se agregan con Contract.Ensures:

Contract.Ensures(Contract.Result<IEnumerable<Type>>() != null);
Contract.Ensures(Contract.ForAll<Type>(
    Contract.Result<IEnumerable<Type>>(),
    (Type item) => item.GetInterface(typeName) != null));

La primera poscondición especifica que el resultado nunca es null. Esto implica que cuando no se encuentran los tipos buscados en el ensamblado indicado, el método retorna una lista vacía y no null [2].

La segunda poscondición también incluye un cuantificador universal. En prosa, la poscondición especifica que todos los tipos retornados como resultado, implementan la interfaz pasada como argumento, ¡qué es justamente lo que dije que el método hacía cuando lo describí más arriba!

Esa es justamente la belleza del diseño por contrato: es posible describir lo que un programa hace (un método en este caso) de forma inequívoca, no ambigua, y comprobable automáticamente. Además, como siempre acompaña al código, es más fácil de encontrar y mantener actualizada, no se pierde, etc.

Pero volvamos a Pex. Para guardar los casos de prueba generados, los seleccionamos, hacemos clic con el botón secundario del mouse, y luego elegimos Save…:

image

Pex nos muestra lo que va a hacer a continuación, paso a paso:

image

Luego que Pex realiza los cambios anunciados, la solución tiene ahora un proyecto de prueba, que podemos ejecutar igual que cualquier otro:

image

Antes de ejecutar los casos de prueba generados por Pex, vamos hablar un poco de cobertura.

Uno de los objetivos de usar Pex es generar la menor cantidad casos de prueba que aseguren la mayor cobertura posible. La cobertura de los casos de prueba, o simplemente cobertura, es la cantidad de líneas de código alcanzadas durante la ejecución de la prueba. Si la cobertura es baja, faltan casos de prueba y podría haber errores en las líneas de código no alcanzadas durante la prueba. No podemos afirmar que haya errores, pero sí que si hay errores en las líneas no alcanzadas no podremos encontrarlos.

Por el contrario, una mayor cobertura no es garantía de mejores casos de prueba o ausencia de errores, pero si me dan a elegir a mi prefiero la mayor cobertura posible.

Para medir la cobertura es necesario cambiar primero la configuración de ejecución las pruebas. Hacemos clic en Test, luego en Edit Test Run Configurations y luego en la configuración deseada, que en forma predeterminada es Local Test Run:

image

Allí seleccionamos Code Coverage de la lista y marcamos el ensamblado del que queremos medir la cobertura en la lista Select artifacts to instrument.

Para ejecutar los casos de prueba hacemos clic en Test, luego en Run, y luego All Tests in Solution. Al final obtenemos el resultado:

image

No sabemos todavía si el método LoadTypeFrom funciona o no. Para averiguarlo podemos generar un caso de prueba que busque una clase que implemente una interfaz que sabemos que está en cierto ensamblado. Agregamos en el mismo lugar donde está la clase de prueba generada por Pex, una interfaz y una clase que implemente esa interfaz, como estas:

public interface Foo { }

public class Boo : Foo { }
}

Luego agregamos un nuevo método así:

[TestMethod]
public void TestLoadTypesFrom()
{
    IEnumerable<Type> result = Helper.LoadTypesFrom(
    Assembly.GetExecutingAssembly().Location, "Foo");
    bool booExists = false;
    foreach (Type type in result)
    {
        if (type.Equals(typeof(Boo)))
        {
            booExists = true;
        }
    }
    PexAssert.IsTrue(booExists);
}

Como la interfaz Foo y la clase Boo están implementadas junto con los casos de prueba, están en el mismo ensamblado, el ensamblado que se está ejecutando cuando se invoca el caso de prueba. Ese ensamblado se obtiene con Assembly.GetExecutingAssembly. Por eso podemos estar seguros que el método LoadTypesFrom debería retornar Boo en la lista. El bucle foreach busca el tipo Boo en el resultado y usa PexAssert.IsTrue para mostrar el resultado junto con el resultado de los demás casos de pruebas generados por Pex.

image

Conclusiones

Pex es un proyecto de Microsoft Research, que se integra directamente en Visual Studio. Realiza un análisis sistemático del código a probar, buscando valores de frontera, excepciones, etc. y así encuentra valores de entrada/salida interesantes para los métodos seleccionados, que pueden ser usados para generar una pequeña suite de pruebas con alta cobertura de código.

Contracts es también un proyecto de Microsoft Research para poder aplicar conceptos de diseño por contrato en los lenguajes del .NET Framework.

Cada uno por su lado es muy interesante, pero usados juntos, podemos crear mejores especificaciones con Contracts y mejores casos de prueba con Pex. Por eso decimos, Pex y Contacts: ¡juntos son dinamita!

Referencias

La página principal de Pex en Microsoft Research es http://research.microsoft.com/en-us/projects/pex/

En MSDN DevLabs es http://msdn.microsoft.com/en-us/devlabs/cc950525.aspx

La página principal de Contracts en Microsoft Research es http://research.microsoft.com/en-us/projects/contracts/

En MSDN DevLabs es http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx


[1] Alguien podría decir que no es correcto agregar esta precondición pues no indica una condición sobre el programa sino sobre un archivo que es externo al programa. Lo que estamos especificando es que el ensamblado debe existir para poder extraer tipos de él.

[2] Es discutible si el resultado debería ser null en lugar de una lista vacía. Alguien podría decir que se está instanciando un objeto que luego no se usa, lo que impacta negativamente en el desempeño. Yo prefiero el enfoque de retornar la lista vacía, pues siempre puedo poner el resultado en un bucle foreach sin miedo a que pueda fallar porque la colección a recorrer vale null, en este caso donde el impacto en el desempeño es mínimo.

About these ads

Written by fernandomachadopiriz

07/05/2010 at 16:25

4 comentarios

Subscribe to comments with RSS.

  1. [...] buen amigo Fernando Machado escribió un excelente artículo, aquí el detalle: Pex y Contracts: Juntos son dinamita Pex es un generador automático de pruebas de unidad que se integra con Visual Studio. Contracts es [...]

  2. Artículo claro y 100% didáctico. Thanks for sharing!

    Alvaro Regalado

    13/05/2010 at 13:21

  3. [...] Pueden encontrar más detalles sobre Pex, incuyendo código ejemplo, en este otro artículo. [...]

  4. [...] Contracts es una herramienta originada en Microsoft Research para Visual Studio que implementa diseño por contrato en .NET Framework. Al usar Contacts, Pex genera mejores casos de prueba. Para obtener más detalles sobre cómo usar Pex y Contracts juntos, lean este artículo. [...]


Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

Únete a otros 222 seguidores

A %d blogueros les gusta esto: