Compartir a través de


C++ ágil

Desarrollo y pruebas ágiles en C++ con Visual Studio y TFS

John Socha-Leialoha

Descargar el ejemplo de código

Usted es un desarrollador o evaluador que trabaja en una aplicación creada con Visual C++. ¿No le encantaría si como desarrollador pudiera ser más productivo, crear código de mejor calidad, y reescribirlo según sea necesario para mejorar la arquitectura y sin miedo a echar a perder nada? ¿Y como evaluador no le gustaría tener que ocupar menos tiempo en escribir y mantener las pruebas, para tener más tiempo para otras actividades de prueba?

En este artículo presentaré algunas técnicas que emplea nuestro equipo en Microsoft para crear las aplicaciones.

Somos un equipo es bastante pequeño. Tenemos diez personas trabajando en diferentes proyectos a la vez. Estos proyectos están escritos tanto en C# como en C++. Usamos C++ principalmente para los programas que se deben ejecutar dentro de Windows PE, que es una versión reducida de Windows que se emplea en la instalación de sistemas operativos. También se emplea dentro de una secuencia de tareas de Microsoft System Center Configuration Manager para ejecutar tareas que no se pueden ejecutar dentro del sistema operativo completo, como por ejemplo para capturar un disco duro en un archivo de un disco duro virtual (VHD). Esto es bastante trabajo para un equipo tan pequeño, así que tenemos que ser productivos.

Nuestro equipo usa Visual Studio 2010 y Team Foundation Server (TFS) 2010. Empleamos TFS 2010 para el control de versiones, seguimiento del trabajo, integración continua, y para recopilar e informar la cobertura de código.

Cuándo y cómo escribimos las pruebas

Comenzaré por explicar cómo nuestro equipo escribe las pruebas (las respuestas podrían ser diferentes para otros equipos). Las respuestas específicas de nuestros desarrolladores son un poco diferentes de las que entregan nuestros evaluadores, pero probablemente no tan diferentes como se podría creer inicialmente. Estas son mis metas de un desarrollador:

  • Ausencia de errores en la compilación
  • Ausencia de regresiones
  • Poder refactorizar con confianza
  • Poder modificar la arquitectura con confianza
  • Impulsar el diseño mediante un desarrollo guiado por pruebas (TDD)

Por supuesto, la calidad es el gran motivo detrás de estas metas. Cuando estas metas se cumplen la vida del desarrollador es mucho más productiva y entretenida que cuando no se cumplen.

En el caso de los evaluadores me enfocaré en un solo aspecto del evaluador ágil: escribir pruebas automatizadas. Cuando nuestros evaluadores escriben pruebas automatizadas sus metas incluyen: ausencia de regresiones, desarrollo guiado por aceptación y recopilar e informar la cobertura de código.

Pero por supuesto, nuestros evaluadores hacen mucho más que sólo escribir pruebas automatizadas. Son responsables de recopilar la cobertura de código, ya que queremos que los valores que representan la cobertura de código también incluyan los resultados de todas las pruebas, no sólo de las pruebas unitarias (volveremos sobre esto más adelante).

En este artículo cubriré las diferentes herramientas y técnicas que emplea nuestro equipo para lograr todas estas metas.

Eliminar los errores de compilación mediante entradas validadas

En el pasado nuestro equipo creaba ramas para que los evaluadores siempre tuvieran una versión estable para hacer las pruebas. Pero mantener una rama siempre exige un trabajo adicional. Ahora que tenemos entradas validadas, sólo creamos ramas para las publicar las versiones, lo que significa un gran paso adelante.

Para usar las entradas validadas se debe implementar un control de compilación y un agente de compilación, o varios de éstos. No ahondaré sobre este tema aquí, así que si está interesado puede encontrar más detalles en la página de la MSDN Library, “Administrar Team Foundation Build”, en bit.ly/jzA8Ff.

Una vez que tenga en marcha los agentes de compilación, puede crear una nueva definición de compilación para las entradas validadas. Para crearla debe seguir los siguientes pasos dentro de Visual Studio:

  1. Haga clic en Ver en la barra de menús y luego haga clic en Team Explorer para que la ventana de la herramienta Team Explorer esté visible.

  2. Expanda su proyecto de equipo y haga clic con el botón secundario en Compilar.

  3. Haga clic en Definición de nueva compilación.

  4. Haga clic en Desencadenador a la izquierda, y seleccione Entrada validada, como puede observar en la Figura 1.

    Select the Gated Check-in Option for Your New Build Definition

    Figura 1 Seleccione la opción Entrada validada para la definición de su nueva compilación

  5. Haga clic en Valores predeterminados de compilación y seleccione el controlador de compilación.

  6. Haga clic en Procesar y seleccione los elementos que va a compilar.

Después de guardar esta definición de compilación (le pusimos el nombre de “Gated Checkin” a la nuestra) verá un cuadro de diálogo una vez que envíe su entrada (consulte la Figura 2). Al hacer clic en Cambios de compilación se crea un conjunto de cambios aplazados que se envía al servidor de compilación. Si no hay errores de compilación y se superan todas las pruebas unitarias, entonces TFS publicará los cambios. De lo contrario, los rechazará.

Gated Check-in Dialog Box

Figura 2 Cuadro de diálogo de la entrada validada

Las entradas validadas son muy útiles, ya que garantizan que nunca se produzcan errores de compilación. También garantizan que se superen todas las pruebas unitarias. Es muy fácil que un desarrollador olvide ejecutar todas las pruebas antes de publicar los cambios. Pero gracias a las entradas validadas esto es parte del pasado.

Escribir pruebas unitarias para C++

Ahora que ya sabe cómo ejecutar las pruebas unitarias como parte de la entrada validada, veamos cómo se pueden escribir estas pruebas para código nativo en C++.

Tengo muchas razones para ser un gran defensor del desarrollo guiado por pruebas. Me permite concentrarme en el comportamiento, lo que hace que mis diseños resulten más sencillos. Además, las pruebas sirven de red de seguridad, ya que definen un contrato para el comportamiento. Puedo refactorizar sin miedo a introducir errores por infringir accidentalmente las reglas del contrato del comportamiento. Y sé que los demás desarrolladores tampoco van a dañar accidentalmente algún comportamiento requerido.

Uno de los desarrolladores del equipo tenía una forma muy particular de usar el ejecutor de pruebas incorporado (mstest) para evaluar el código C++. Escribía las pruebas unitarias para .NET Framework con código C++/CLI, y éstas llamaban funciones públicas expuestas por una DLL en C++ nativo. El método que presento en esta sección es mucho más amplio aún que esa técnica, ya que permite crear directamente instancias de clases nativas de C++ que forman parte del código de producción. Permite, en otras palabras, evaluar más que sólo la interfaz pública.

La solución está en ubicar el código de producción en una biblioteca estática que se puede vincular tanto con los DLL de las pruebas unitarias como con el EXE o DLL de producción, tal como se puede observar en la <strong>Figura 3</strong>.

Figura 3 Las pruebas y el código de producción comparten el mismo código a través de una biblioteca estática

Estos son los pasos que debe seguir para configurar sus proyectos de acuerdo con este procedimiento. Comience por crear la biblioteca estática:

  1. En Visual Studio, haga clic en Archivo, luego en Nuevo y finalmente en Proyecto.
  2. Haga clic en Visual C++ en la lista de Plantillas instaladas (deberá expandir Otros idiomas).
  3. En la lista de los tipos de proyectos haga clic en Proyecto Win32.
  4. Escriba el nombre del proyecto y haga clic en el botón Aceptar.
  5. Haga clic en Siguiente, luego en Biblioteca estática y finalmente en Finalizar.

Ahora cree el DLL de pruebas. Para configurar un proyecto de pruebas hacen falta algunos pasos más. Deberá crear el proyecto, pero también le deberá proporcionar acceso al código y los archivos de encabezado en la biblioteca estática.

Comience por hacer clic con el botón secundario en la ventana Explorador de soluciones. Haga clic en Agregar, y luego en Nuevo proyecto. Haga clic en Prueba en el nodo Visual C++ en la lista de plantillas. Escriba el nombre del proyecto (nuestro equipo siempre agrega UnitTests al final del nombre del proyecto) y haga clic en Aceptar.

En el Explorador de soluciones haga clic con el botón secundario en el nuevo proyecto, y haga clic en Propiedades. En el árbol a la izquierda haga clic en Propiedades comunes. Haga clic en Agregar nueva referencia. Haga clic en la pestaña Proyectos, seleccione el proyecto con la biblioteca estática y haga clic en Aceptar para descartar el cuadro de diálogo Agregar referencia.

Expanda el nodo Propiedades de configuración en el árbol a la izquierda, luego expanda el nodo C/C++. Haga clic en General bajo el nodo C/C++. Haga clic en el cuadro combinado Configuración y seleccione Todas las configuraciones para asegurarse de cambiar tanto la versión de Depuración como la de Publicación.

Haga clic en Bibliotecas de inclusión adicionales y escriba la ruta de acceso a la biblioteca estática. Deberá reemplazar MiBibEstática por el nombre de su biblioteca estática:

$(SolutionDir)\MiBibEstática;%(AdditionalIncludeDirectories)

En la misma lista de propiedades haga clic en la propiedad Compatibilidad con Common Language Runtime y cámbiela a Compatibilidad con Common Language Runtime (/clr).

Haga clic en la sección General bajo Propiedades de configuración y cambie la propiedad TargetName a $(ProjectName). Está establecida en DefaultTest de manera predeterminada para todos los proyectos de prueba, pero debe ser el nombre del proyecto. Haga clic en Aceptar.

Para agregar la biblioteca estática al EXE o DLL de producción deberá repetir la primera parte de este procedimiento.

Escribir las primeras pruebas unitarias

Ahora debería tener todo lo que necesita para escribir una nueva prueba unitaria. Como los métodos de prueba serán métodos .NET escritos en C++, la sintaxis será un poco diferente del C++ nativo. Si conoce C#, se dará cuenta que se parece en muchos aspectos a una mezcla entre C++ y C#. Para obtener más información, consulte la documentación de MSDN Library, “Características del lenguaje especializadas para el CLR”, en bit.ly/iOKbR0.

Digamos que quiere evaluar una definición de clase similar a la que sigue:

#pragma once
class MyClass {
  public:
    MyClass(void);
    ~MyClass(void);

    int SomeValue(int input);
};

Ahora quiere escribir una prueba que defina un comportamiento específico para el método SomeValue. En la Figura 4 puede observar un ejemplo con el archivo .cpp completo de cómo se podría ver una prueba unitaria sencilla para este método.

Figura 4 Una prueba unitaria sencilla

#include "stdafx.h"
#include "MyClass.h"
#include <memory>
using namespace System;
using namespace Microsoft::VisualStudio::TestTools::UnitTesting;

namespace MyCodeTests {
  [TestClass]
  public ref class MyClassFixture {
    public:
      [TestMethod]
      void ShouldReturnOne_WhenSomeValue_GivenZero() {
        // Arrange
        std::unique_ptr<MyClass> pSomething(new MyClass);
 
        // Act
        int actual = pSomething->SomeValue(0);
 
        // Assert
        Assert::AreEqual<int>(1, actual);
      }
  };
}

Por si no está acostumbrado a escribir pruebas unitarias, aquí empleo un patrón conocido como Acomodar, Actuar, Aprobar (AAA). En la parte Acomodar se establecen las condiciones previas para la situación que se desea evaluar. En Actuar se llaman los métodos que se están evaluando. En Aprobar se corrobora si el método se comportó de la manera deseada. Me gusta agregar un comentario antes de cada sección para mejorar la legibilidad y para poder encontrar la sección Actuar fácilmente.

Los métodos de prueba están marcados con el atributo TestMethod, como se puede ver en la Figura 4. Estos métodos, as su vez, deben encontrarse dentro de una clase marcada con el atributo TestClass.

Observe que la primera línea en el método de prueba crea una nueva instancia de la clase nativa C++. Me gusta usar la clase unique_ptr de la biblioteca estándar de C++, ya que ésta garantiza que la instancia se borra automáticamente una vez que finaliza el método de prueba. Por lo tanto, queda muy claro que se puede mezclar C++ nativo con el código CLI/C++ de .NET. Hay algunas restricciones, por supuesto, que describiré en la siguiente sección.

Nuevamente, si no había escrito pruebas con .NET antes, la clase Assert tiene varios métodos útiles que puede usar para comprobar diferentes condiciones. Me gusta usar la versión genérica para ser explícito acerca de los tipos de datos que espero del resultado.

Sacar provecho de las ventajas de las pruebas en C++/CLI

Tal como lo había mencionado, se deben tener en cuenta algunas limitantes al mezclar código C++ nativo con código C++/CLI. Las diferencias provienen de las diferentes formas en que los dos tipos de código administran la memoria. El código C++ nativo emplea el operador new de C++ para asignar memoria, y el programador es responsable de liberar esa memoria. Una vez que se asigna una porción de la memoria, los datos siempre residirán en el mismo lugar.

Por el otro lado, los punteros en el código C++/CLI tienen un comportamiento muy diferente que se debe al modelo de recolección de elementos no utilizados que el modelo hereda de .NET Framework. Para crear nuevos objetos .NET en C++/CLI se emplea el operador gcnew en vez del operador new; éste devuelve un identificador de objeto en vez de un puntero al objeto. Un identificador en esencia consiste en un puntero a otro puntero. Cada vez que el sistema de recolección de elementos no utilizados desplaza los objetos dentro de la memoria, actualiza los identificadores con las nuevas ubicaciones.

Hay que tener mucho cuidado al mezclar los punteros nativos con los punteros administrados. Examinaré algunas de estas diferencias, y entregaré algunas sugerencias y trucos para sacar mayor provecho de las pruebas C++/CLI para los objetos C++ nativos.

Digamos que usted quiere probar un método que devuelve un puntero a una cadena. En C++ podría representar el puntero con LPCTSTR. Pero en .NET las cadenas se representan por String^ en C++/CLI. El signo circunflejo después del nombre de la clase representa un identificador para un objeto administrado.

El siguiente ejemplo muestra cómo se podría probar el valor de una cadena devuelta por la llamada al método:

// Act
LPCTSTR actual = pSomething->GetString(1);
 
// Assert
Assert::AreEqual<String^>("Test", gcnew String(actual));

La última línea es la que contiene todos los detalles. Existe un método AreEqual que acepta cadenas administradas, pero no hay un método equivalente para las cadenas nativas en C++. Por lo tanto, hay que usar cadenas administradas. El primer parámetro del método AreEqual es una cadena administrada, de modo que en verdad es una cadena Unicode, aunque no se haya usado _T o L, por ejemplo, para marcarla como cadena Unicode.

La clase String tiene un constructor que acepta una cadena C++, de modo que permite crear una nueva cadena administrada que contiene el valor del método que se está probando, y en ese momento AreEqual corrobora que tengan el mismo valor.

La clase Assert tiene dos métodos que podrían parecer útiles: IsNull e IsNotNull. Pero el parámetro para estos métodos es un identificador, no un puntero a un objeto, y por lo tanto sólo se pueden usar con los objetos administrados. Pero en vez de esto se puede usar el método IsTrue del siguiente modo:

Assert::IsTrue(pSomething != nullptr, "Should not be null");

Esto realiza la misma tarea con un poco más de código. El comentario que agregué aparece en el resultado de la prueba y deja en claro el resultado esperado, como se puede observar en la Figura 5.

Test Results Showing the Additional Comment in the Error Message

Figura 5 Resultados de la prueba con el comentario adicional en el mensaje de error

Compartir el código de inicialización y desmontaje

El código de prueba se debe tratar igual que el código de producción. Es decir, es necesario refactorizar las pruebas al igual que el código de producción, para que el código de prueba sea fácil de mantener. Es probable que en algún momento tenga algún código de inicialización y de desmontaje que sea común a todos los métodos de una clase de prueba. Puede designar un método que se ejecutará antes de cada prueba, junto con un método que se ejecuta después de cada prueba (puede tener sólo uno de éstos, ambos, o ninguno).

El atributo TestInitialize identifica un método que se ejecutará antes de cada método de prueba en la clase de prueba. De igual modo, TestCleanup es un método que se ejecuta después de cada método de prueba en la clase de prueba. Aquí se muestra un ejemplo:

[TestInitialize]
void Initialize() {
  m_pInstance = new MyClass;
}
 
[TestCleanup]
void Cleanup() {
  delete m_pInstance;
}

MyClass *m_pInstance;

Antes que nada, observe que m_pInstance es un simple puntero a la clase. ¿Por qué no empleé unique_ptr para administrar la duración del objeto?

La respuesta nuevamente tiene que ver con la mezcla de código nativo C++ y C++/CLI. En C++/CLI las variables de instancia forman parte de un objeto administrado, y por lo tanto sólo pueden consistir en identificadores de objetos administrados, punteros a objetos nativos o tipos de valores. Hay que regresar a los operadores básicos new y delete para administrar la duración de las instancias C++ nativas.

Usar punteros a las variables de instancia

Si emplea COM en alguna situación quizás podría sentirse tentado de escribir algo parecido a esto:

[TestMethod]
Void Test() {
  ...
  HRESULT hr = pSomething->GetWidget(&m_pUnk);
  ...
}

IUnknown *m_pUnk;

Esto no compilará, y producirá un mensaje de error similar a este:

no se puede convertir el parámetro 1 de 'cli::interior_ptr<Type>' a 'IUnknown **'

En este caso la dirección de una variable de instancia C++/CLI es del tipo interior_ptr<IUnknown *>, y este tipo no es compatible con el código nativo C++. Puede que se pregunte por qué, ya que sólo quería un puntero.

La clase de prueba es una clase administrada, y por lo tanto el recolector de elementos no utilizados puede desplazar las instancias de esta clase dentro de la memoria. Por lo tanto, en el caso de un puntero a una variable de instancia, al desplazar el objeto el puntero dejaría de ser válido.

Puede bloquear el objeto durante el tiempo que dure la llamada nativa del siguiente modo:

cli::pin_ptr<IUnknown *> ppUnk = &m_pUnk;
HRESULT hr = pSomething->GetWidget(ppUnk);

La primera línea bloquea la instancia hasta que la variable salga del ámbito, y esto permite pasar un puntero a la variable de instancia a una clase C++ nativa, aunque la variable esté contenida dentro de una clase de prueba administrada.

Escribir código fácil de probar

Al comienzo de este artículo mencioné la importancia de escribir código que sea fácil de probar. Yo empleo el desarrollo guiado por pruebas para asegurarme de que el código se pueda probar, pero algunos desarrolladores prefieren escribir las pruebas después de escribir el código. En ambos casos, es importante no sólo pensar en las pruebas unitarias, sino que en todo el proceso de prueba.

Mike Cohn, un autor conocido y prolífico sobre el método de desarrollo ágil, creó una pirámide acerca de la automatización de las pruebas que entrega una idea de los tipos de pruebas y de cuántas debería haber en cada nivel. Los desarrolladores deberían escribir todas o la mayoría de las pruebas unitarias y de componentes, y quizás algunas pruebas de integración. Para obtener más información acerca de esta pirámide, consulte la publicación en el blog de Cohn, “El nivel olvidado de la pirámide de la automatización de pruebas” (bit.ly/eRZU2p).

Los evaluadores generalmente son responsables de escribir las pruebas de aceptación y de IU. Éstas a veces también se suelen llamar pruebas de extremo a extremo o E2E. En la pirámide de Cohn, el triángulo de la IU es el más pequeño, si se compara con las áreas de los otros tipos de pruebas. Lo ideal es escribir la menor cantidad de pruebas automatizadas de IU posible. Las pruebas automatizadas de IU suelen ser frágiles y costosas de escribir y mantener. Los cambios más pequeños en la IU pueden generar errores en las pruebas de IU.

Si el código no está escrito para ser probado, es muy fácil terminar con una pirámide invertida, donde la mayoría de las pruebas automatizadas son pruebas de IU. Esta situación no es deseable, pero lo que hay que recordar es que el desarrollador es el responsable de que los evaluadores puedan escribir pruebas de integración y de aceptación en los niveles inferiores a la IU.

Además, por alguna razón la mayoría de los evaluadores que he conocido se sienten muy cómodos al escribir pruebas en C#, pero tratan de evitar hacerlo en C++. Como resultado de esto, nuestro equipo tuvo que encontrar un puente entre el código C++ bajo prueba y las pruebas automatizadas. El puente consiste en los contextos, que son unas clases C++/CLI que a la vista del código C# se ven iguales a cualquier otra clase administrada.

Crear contextos de C# a C++

Estas técnicas no difieren en mucho de las que describí en el caso de las pruebas C++/CLI. Ambos usan el mismo tipo de código en modo mixto. La diferencia está en cómo se usan al final.

Lo primero que debe hacer es crear un nuevo proyecto que contiene los contextos:

  1. En el Explorador de soluciones haga clic con el botón secundario en el nodo de la solución, luego en Agregar y finalmente en Nuevo proyecto.
  2. Bajo Otros idiomas, Visual C++, CLR haga clic en Biblioteca de clases.
  3. Escriba el nombre del proyecto y haga clic en Aceptar.
  4. Repita estos pasos para crear un proyecto de prueba para agregar una referencia y los archivos de inclusión.

La clase de contexto misma se parecerá un poco a la clase de prueba, pero no tendrá todos los atributos (consulte la Figura 6).

Figura 6 Contextos de prueba de C# a C++

#include "stdafx.h"
#include "MyClass.h"
using namespace System;
 
namespace MyCodeFixtures {
  public ref class MyCodeFixture {
    public:
      MyCodeFixture() {
        m_pInstance = new MyClass;
      }
 
      ~MyCodeFixture() {
        delete m_pInstance;
      }
 
      !MyCodeFixture() {
        delete m_pInstance;
      }
 
      int DoSomething(int val) {
        return m_pInstance->SomeValue(val);
      }
 
      MyClass *m_pInstance;
  };
}

Observe que no hay ningún archivo de encabezado. Esta es una de las características que más aprecio de C++/CLI. Como esta biblioteca de clases crea un ensamblado administrado, la información acerca de las clases se almacena como información del tipo .NET, y por lo tanto no se necesitan archivos de encabezado.

Esta clase también contiene un destructor y un finalizador. Aquí el destructor en verdad no es el destructor. En vez de esto, el compilador reescribe el destructor en forma de implementación del método Dispose de la interfaz IDisposable. Por lo tanto, cualquier clase C++/CLI que tenga un destructor implementa la interfaz IDisposable.

El método !MyCodeFixture es el finalizador. Éste es llamado por el recolector de elementos no utilizados cuando decide que es hora de eliminar el objeto, a menos que usted previamente haya llamado el método Dispose. Puede emplear la instrucción using para controlar la duración del objeto nativo C++ incrustado, o puede dejar que el recolector de elementos no utilizados se haga cargo de esto. Puede encontrar más información acerca de este comportamiento en el artículo de MSDN Library, “Cambios en la semántica de los destructores” en bit.ly/kW8knr.

Una vez que tiene una clase de contexto en C++/CLI, puede escribir una prueba unitaria en C# parecida a la que se puede ver en la Figura 7.

Figura 7 Un sistema de pruebas unitarias en C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyCodeFixtures;
 
namespace MyCodeTests2 {
  [TestClass]
  public class UnitTest1 {
    [TestMethod]
    public void TestMethod1() {
      // Arrange
      using (MyCodeFixture fixture = new MyCodeFixture()) {
        // Act
        int result = fixture.DoSomething(1);
 
        // Assert
        Assert.AreEqual<int>(1, result);
      }
    }
  }
}

Personalmente me gusta emplear la instrucción using para controlar en forma explícita la duración de los objetos de contexto en vez de usar el recolector de elementos no utilizados. Esto es especialmente importante en los métodos de prueba, para garantizar que las diferentes pruebas no interactúen entre ellas.

Recopilar e informar la cobertura de código

El último punto que mencioné al principio de este artículo fue la cobertura de código. El primer paso de nuestro equipo fue recopilar la cobertura de código automáticamente con el servidor de compilación, y publicarlo en TFS para que estuviera disponible fácilmente para todos.

Lo primero que hice fue descubrir cómo recopilar la cobertura de código C++ de la ejecución de pruebas. En Internet encontré una entrada de blog instructiva de Emil Gustafsson llamada “Informes de cobertura de código nativo C++ mediante Visual Studio 2008 Team System” (bit.ly/eJ5cqv). Esta publicación muestra los pasos necesarios para recopilar la información de cobertura de código. Esto lo convertí en un archivo CMD que puedo ejecutar en cualquier momento en mi máquina de desarrollo para recopilar la información de cobertura de código:

"%VSINSTALLDIR%\Team Tools\Performance Tools\vsinstr.exe" Tests.dll /COVERAGE
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /START:COVERAGE /WaitStart /OUTPUT:coverage
mstest /testcontainer:Tests.dll /resultsfile:Results.trx
"%VSINSTALLDIR%\Team Tools\Performance Tools\vsperfcmd.exe" /SHUTDOWN

Para usarlo en su sistema deberá reemplazar Tests.dll con el nombre del DLL que contenga sus pruebas. También deberá preparar los DLL para la instrumentación:

  1. En el Explorador de soluciones haga clic con el botón secundario en el proyecto de prueba.
  2. Haga clic en Propiedades.
  3. Seleccione la Configuración de depuración.
  4. Expanda las Propiedades de configuración, luego expanda Vinculador y haga clic en Avanzado.
  5. Cambie la propiedad Perfil a Sí (/PROFILE).
  6. Haga clic en Aceptar.

Estos pasos habilitan la generación de perfiles que debe estar activada para instrumentar los ensamblados, para poder recopilar la información de cobertura de código.

Vuelva a compilar el proyecto y ejecute el archivo CMD. Esto debería crear un archivo de cobertura. Cargue este archivo de cobertura en Visual Studio para asegurarse de que es capaz de recopilar la cobertura de código desde sus pruebas.

Para realizar estos pasos en el servidor de compilación y publicar los resultados en TFS necesita una plantilla de compilación personalizada. Las plantillas de compilación de TFS se almacenan en el sistema de control de versiones y pertenecen a un proyecto de equipo específico. Encontrará una carpeta llamada BuildProcessTemplates bajo cada proyecto de equipos, y ésta probablemente tendrá varias plantillas de compilación.

Para usar la plantilla de compilación personalizada que se encuentra en el ejemplo de código, abra la ventana Explorador de control de código fuente. Navegue a la carpeta BuildProcessTemplates en el proyecto de equipo y asegúrese de asignarlo a un directorio en su equipo. Copie el archivo BuildCCTemplate.xaml a esta ubicación asignada. Agregue esta plantilla al control de código fuente y publíquelo.

Los archivos de las plantillas deben estar publicados antes de poder usarlos en las definiciones de compilación.

Ahora que la plantilla de compilación ya está publicada, puede crear una definición de compilación para ejecutar la cobertura de código. La información de cobertura de código C++ se recopila mediante el comando vsperfmd, tal como se mostró anteriormente. Mientras vsperfmd está en ejecución, escucha en búsqueda de información de cobertura de código de todos los ejecutables instrumentados que se ejecutan. Por lo tanto no podrá ejecutar otras pruebas instrumentadas al mismo tiempo. También deberá asegurarse de que se esté ejecutando sólo un agente de compilación en la máquina que procesa estas ejecuciones de cobertura de código.

Yo creé una definición de compilación que se ejecuta todas las noches. Usted puede hacer lo mismo del siguiente modo:

  1. En la ventana Team Explorer expanda el nodo de su proyecto de equipo.
  2. Haga clic con el botón segundario en Compilaciones, que es un nodo bajo su proyecto de equipo.
  3. Haga clic en Definición de nueva compilación.
  4. En la sección Desencadenador haga clic en Programación y seleccione los días en que quiere ejecutar la cobertura de código.
  5. En la sección Proceso haga clic en Mostrar detalles en la sección llamada Plantilla de proceso de compilación en la parte superior, y luego seleccione la plantilla de compilación que publicó en el control de código fuente.
  6. Complete las otras secciones requeridas y guarde.

Agregar un archivo de configuración de pruebas

La definición de compilación también requiere de un archivo de configuración de pruebas. Esto es un archivo XML que enumera todos los DLL para los que desea recopilar y publicar resultados. Para crear este archivo para la cobertura de código debe seguir los siguientes pasos:

  1. Haga doble clic en el archivo de configuración Local.test para abrir el cuadro de diálogo Configuración de pruebas.
  2. En la lista al lado izquierdo haga clic en Datos y diagnósticos.
  3. Haga clic en Cobertura de código y active la casilla.
  4. Haga clic en el botón Configurar que se encuentra encima de la lista.
  5. Active la casilla al lado del DLL que contiene sus pruebas (y también contiene el código que se está probando).
  6. Desactive Instrumentar ensamblados en contexto, ya que la definición de configuración se hará cargo de esto.
  7. Haga clic en Aceptar y luego en Cerrar.

Si desea compilar más de una solución, o si tiene más de un proyecto de prueba, necesitará una copia del archivo de configuración de pruebas que incluya los nombres de todos los ensamblados que deberán ser examinados para la cobertura de código.

Para esto, copie el archivo de configuración de pruebas a la raíz de la rama y póngale un nombre descriptivo, como por ejemplo CC.testsettings. Edite el XML. El archivo contendrá al menos un elemento CodeCoverageItem que provendrá de los pasos previos. Tendrá que agregar un elemento para cada DLL que desee examinar. Observe que las rutas de acceso son relativas a la ubicación del archivo del proyecto, no al archivo de la configuración de pruebas. Publique el archivo en el sistema de control de código fuente.

Finalmente deberá modificar la definición de compilación para emplear este archivo de configuración:

  1. En la ventana Team Explorer expanda el nodo de su proyecto de equipo, y luego expanda Compilaciones.
  2. Haga clic con el botón secundario en la definición de compilación creado anteriormente.
  3. Haga clic en Editar definición de compilación.
  4. In la sección Proceso expanda Pruebas automatizadas, luego 1. Ensamblado de prueba y haga clic en Archivo de configuración de prueba. Haga clic en el botón … y seleccione el archivo de configuración de pruebas creado anteriormente.
  5. Guarde los cambios.

Para probar esta definición de compilación debe hacer clic con el botón secundario y seleccionar Poner nueva compilación en cola para iniciar inmediatamente una nueva compilación.

Informar la cobertura de código

Creé un informe personalizado para SQL Server Reporting Services que presenta la cobertura de código, como se puede observar en la Figura 8 (oculté los nombres de los proyectos para proteger a los culpables). Este informe emplea una consulta SQL para leer los datos dentro del almacén de TFS y presentar los resultados combinados para el código C++ y C#.

The Code-Coverage Report

Figura 8 El informe de la cobertura de código

No revisaré en detalle cómo funciona este informe, pero hay algunos aspectos que quisiera mencionar. La base de datos contiene demasiada información acerca de la cobertura de código, por dos razones: el código de los métodos de prueba está incluido en los resultados, y las bibliotecas estándar de C++ (las que se encuentran en los archivos de encabezado) están incluidas en los resultados.

El código de la consulta SQL filtra estos datos innecesarios. Este es el código SQL que se encuentra dentro del informe:

    and CodeElementName not like 'std::%'
    and CodeElementName not like 'stdext::%'
    and CodeElementName not like '`anonymous namespace'':%'
    and CodeElementName not like '_bstr_t%'
    and CodeElementName not like '_com_error%'
    and CodeElementName not like '%Tests::%'

Estas líneas excluyen los resultados de la cobertura de código para algunos espacios de nombres específicos (std, stdext y anonymous), más un par de clases que vienen con Visual C++ (_bstr_t y _com_error), además de cualquier código que se encuentre en un espacio de nombres que termine con Tests.

Este último filtro, el que elimina los espacios de nombres que terminan con Tests, excluye todos los métodos de las clases de prueba. Como al crear un nuevo proyecto de prueba el nombre del proyecto termina en Tests, todas las clases estarán dentro de un espacio de nombres que termina con Tests de manera predeterminada. Además puede agregar otras clases o espacios de nombres que quiera excluir.

Esto es sólo una breve pincelada de las posibilidades. Siga nuestro progreso en mi blog en blogs.msdn.com/b/jsocha.           

John Socha-Leialoha es desarrollador en el grupo de Plataformas de administración y Entrega de servicios en Microsoft. Dentro de sus logros pasados se encuentran haber creado Norton Commander (en C y lenguaje ensamblador) y haber escrito “Peter Norton’s Assembly Language Book” (Brady, 1987).

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Rong Lu