Agile C++

Desenvolvimento e testes do Agile C++ com o Visual Studio e o TFS

John Socha-Leialoha

Baixar o código de exemplo

Você é um desenvolvedor ou testador trabalhando em um aplicativo criado no Visual C++. Como desenvolvedor, não seria ótimo se você pudesse ser mais produtivo, produzir códigos com mais qualidade e reescrever seus códigos quando necessário para aprimorar a arquitetura sem medo de quebrar alguma coisa? E como testador, você não adoraria passar menos tempo escrevendo e mantendo testes para ter mais tempo para outras atividades de teste?

Neste artigo, apresentarei várias técnicas que a nossa equipe aqui na Microsoft tem usado para criar aplicativos.

Nossa equipe é bem pequena. Temos 10 pessoas trabalhando em três projetos diferentes ao mesmo tempo. Esses projetos são escritos em C# e C++. O código C++ é, na maioria dos casos, para os programas que devem ser executados dentro do Windows PE, que é uma versão simplificada do Windows, frequentemente usada para a instalação de SO. Ele também é usado como parte de uma sequência de tarefas do Microsoft System Center Configuration Manager para executar tarefas que não podem ser executadas dentro do SO completo, como a captura de um disco rígido para um arquivo de disco rígido virtual (VHD). Isso é muita coisa para uma equipe pequena, por isso, precisamos ser produtivos.

Nossa equipe usa o Visual Studio 2010 e o Team Foundation Server (TFS) 2010. Usamos o TFS 2010 para controle de versão, controle do trabalho, integração contínua, e coleta e geração de relatórios de cobertura de código.

Quando e por que nossa equipe escreve testes

Começarei observando por que a nossa equipe escreve testes (as respostas podem ser diferentes para a sua equipe). A resposta específica é um pouco diferente para os nossos desenvolvedores e testadores, mas talvez não seja tão diferente quanto você pensava a princípio. Essas são as minhas metas como desenvolvedor:

  • Sem interrupções na compilação
  • Sem regressões
  • Refatorar com confiança
  • Modificar a arquitetura com confiança
  • Controlar o design por meio de desenvolvimento orientado a testes (TDD)

É claro que a qualidade é o grande “porquê” por trás dessas metas. Quando essas metas são atingidas, a vida do desenvolvedor se torna muito mais produtiva e divertida do que quando isso não acontece.

Para os nossos testadores, me concentrarei em apenas um aspecto de um testador Agile: escrita de testes automatizados. As metas dos nossos testadores quando escrevem testes automatizados incluem: sem regressões, desenvolvimento orientado à aceitação, e coleta e geração de relatórios de cobertura de código.

É claro que os nossos testadores fazem muito mais do que apenas escrever testes automatizados. Nossos testadores são responsáveis pela coleta de cobertura de código porque queremos que os números de cobertura de código incluam os resultados de todos os testes, e não apenas dos testes de unidade (veja detalhes mais adiante).

Neste artigo, abordarei as diferentes ferramentas e técnicas que a nossa equipe usa para atingir as metas descritas aqui.

Eliminação das interrupções na compilação com check-ins controlados

No passado, nossa equipe usava ramificações para garantir que os nossos testadores sempre teriam uma compilação estável para testar. No entanto, há sobrecarga associada à manutenção das ramificações. Agora que temos check-ins controlados, usamos as ramificações apenas para lançamentos, o que é uma boa mudança.

O uso de check-ins controlados exige que você tenha configurado o controle de compilação e um ou mais agentes de compilação. Não abordarei esse tópico aqui, mas você pode encontrar detalhes na página da Biblioteca MSDN, “Administrando o Team Foundation Build”, em bit.ly/jzA8Ff.

Quando os agentes de compilação estiverem configurados e em execução, você poderá criar uma nova definição de compilação para check-ins controlados ao seguir estas etapas no Visual Studio:

  1. Clique em View (Exibir) na barra de menus e clique em Team Explorer para garantir que a janela de ferramentas do Team Explorer está visível.

  2. Expanda seu projeto de equipe e clique com o botão direito do mouse em Build (Compilação).

  3. Clique em New Build Definition (Nova Definição de Compilação).

  4. Clique em Trigger (Disparador) à esquerda e selecione Gated Check-in (Check-in Controlado), conforme mostrado na Figura 1.

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

    Figura 1 Selecione a opção de check-in controlado para sua nova definição de compilação

  5. Clique em Build Defaults (Padrões da Compilação) e selecione o controlador de compilação.

  6. Clique em Process (Processo) e selecione os itens para compilar.

Quando tiver salvo essa definição de compilação — chamamos a nossa de “Gated Checkin” — você verá uma nova caixa de diálogo depois que tiver enviado seu check-in (veja a Figura 2). Clicar em Build Changes (Alterações na Compilação) cria um check-in particular e o envia para o servidor de compilação. Se não houver erros de compilação e os testes de unidade obtiverem êxito, o TFS fará o check-in das suas alterações para você. Caso contrário, ele rejeitará o check-in.

Gated Check-in Dialog Box

Figura 2 Caixa de diálogo do check-in controlado

Os check-ins controlados são realmente bons porque garantem que você nunca terá interrupções na compilação. Eles também garantem que todos os testes de unidade obterão êxito. É muito fácil um desenvolvedor se esquecer de executar todos os testes antes do check-in. Mas com os check-ins controlados, isso é coisa do passado.

Escrita de testes de unidade C++

Agora que você já sabe como executar seus testes de unidade como parte de um check-in controlado, vejamos uma forma de escrever esses testes de unidade para código C++ nativo.

Sou um grande fã do TDD por vários motivos. Ele ajuda a me concentrar no comportamento, o que mantém meus designs mais simples. Também tenho uma rede de segurança na forma de testes que definem o contrato comportamental. Posso refatorar sem medo de introduzir bugs que são um resultado da violação acidental do contrato comportamental. E sei que algum outro desenvolvedor não quebrará um comportamento exigido que ele desconhecia.

Um dos desenvolvedores na equipe tinha uma forma de usar o executor de testes interno (mstest) para testar o código C++. Ele estava escrevendo testes de unidade do Microsoft .NET Framework usando C++/CLI que chamava funções públicas expostas por um C++ DLL nativo. O que apresento nesta seção desenvolve muito mais essa abordagem, permitindo que você crie uma instância diretamente das classes C++ que são internas ao seu código de produção. Em outras palavras, você pode testar mais do que apenas a interface pública.

A solução é colocar o código de produção em uma biblioteca estática que possa ser vinculada às DLLs de teste de unidade, bem como ao EXE ou DLL de produção, conforme mostrado na Figura 3.

Figura 3 Os testes e o produto compartilham o mesmo código via uma biblioteca estática

Estas são as etapas necessárias para configurar seus projetos para seguir esse procedimento. Comece criando a biblioteca estática:

  1. No Visual Studio, clique em File (Arquivo), New (Novo) e, em seguida, em Project (Projeto).
  2. Clique em Visual C++ na lista Installed Templates (Modelos Instalados). Você precisará expandir Other Languages (Outras Linguagens).
  3. Clique em Win32 Project (Projeto do Win32) na lista de tipos de projeto.
  4. Digite o nome do seu projeto e clique em OK.
  5. Clique em Next (Avançar), Static Library (Biblioteca Estática) e, em seguida, em Finish (Concluir).

Agora, crie o DLL de teste. A configuração de um projeto de teste exige mais algumas etapas. Você precisa criar o projeto, mas também fornecer acesso ao código e arquivos de cabeçalho na biblioteca estática.

Comece clicando com o botão direito do mouse na solução na janela Solution Explorer. Clique em Add (Adicionar) e, em seguida, clique em New Project (Novo Projeto). Clique em Test (Teste) no nó Visual C++ na lista de modelos. Digite o nome do projeto (nossa equipe adiciona UnitTests ao final do nome do projeto) e clique em OK.

Clique com o botão direito do mouse no novo projeto em Solution Explorer e clique em Properties (Propriedades). Clique em Common Properties (Propriedades Comuns) na janela à esquerda. Clique em Add New Reference (Adicionar Nova Referência). Clique na guia Projects (Projetos), selecione o projeto com sua biblioteca estática e clique em OK para descartar a caixa de diálogo Add Reference (Adicionar Referência).

Expanda o nó Configuration Properties (Propriedades de Configuração) na árvore à esquerda e, em seguida, expanda o nó C/C++. Clique em General (Geral) no nó C/C++. Clique na caixa de combinação Configuration (Configuração) e selecione All Configurations (Todas as Configurações) para garantir que você alterará as versões Debug (Depuração) e Release (Lançamento).

Clique em Additional Include Libraries (Bibliotecas de Inclusão Adicionais) e digite um caminho para sua biblioteca estática, onde você precisará substituir o nome da sua biblioteca estática por MyStaticLib:

$(SolutionDir)\MyStaticLib;%(AdditionalIncludeDirectories)

Clique na propriedade Common Language Runtime Support (Suporte a Common Language Runtime) na mesma lista de propriedades e altere-a para Common Language Runtime Support (/clr).

Clique na seção General (Geral) em Configuration Properties (Propriedades de Configuração) e altere a propriedade TargetName para $(ProjectName). Por padrão, ela está definida como DefaultTest para todos os projetos de teste, mas deve ser o nome do seu projeto. Clique em OK.

Para adicionar a biblioteca estática ao seu EXE ou DLL de produção, você deve repetir a primeira parte deste procedimento.

Como escrever o seu primeiro teste de unidade

Agora, você já deve ter tudo que precisa pronto para escrever um novo teste de unidade. Seus métodos de teste serão os métodos .NET escritos em C++, por isso, a sintaxe será um pouco diferente do que a do C++ nativo. Se você conhece C#, descobrirá que é uma mistura entre a sintaxe do C++ e C# em vários aspectos. Para obter mais detalhes, consulte a documentação da Biblioteca MSDN, “Recursos de linguagem direcionados para o CLR”, em bit.ly/iOKbR0.

Digamos que uma definição de classe que você testará é mais ou menos parecida com esta:

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

    int SomeValue(int input);
};

Agora, você quer escrever um teste para o método SomeValue para especificar o comportamento desse método. A Figura 4 mostra como pode ser a aparência de um teste de unidade simples, mostrando todo o arquivo .cpp.

Figura 4 Um teste de unidade simples

#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);
      }
  };
}

Se você não estiver familiarizado com a escrita de testes de unidade, estou usando um padrão conhecido como Arrange, Act, Assert. A parte Arrange configura as pré-condições para o cenário que você quer testar. Act é quando você chama o método que está sendo testado. Assert é quando você verifica que o método se comportou da forma que você deseja. Gosto de adicionar um comentário na frente de cada seção para melhorar a legibilidade, e para que seja mais fácil encontrar a seção Act.

Os métodos de teste são marcados pelo atributo TestMethod, conforme pode ser visto na Figura 4. Esses métodos, por sua vez, devem estar contidos em uma classe marcada com o atributo TestClass.

Observe que a primeira linha no método de teste cria uma nova instância da classe C++ nativa. Gosto de usar a classe da biblioteca C++ padrão unique_ptr para garantir que essa instância será excluída automaticamente no final do método de teste. Portanto, você pode ver claramente que é possível misturar o C++ nativo com o seu código CLI/C++ .NET. É claro que há restrições, que descreverei na próxima seção.

Mais um vez, se você nunca tiver escrito testes em .NET antes, a classe Assert tem vários métodos úteis que você pode usar para verificar condições diferentes. Gosto de usar a versão genérica para ser explícito sobre o tipo de dados que espero do resultado.

Como aproveitar por completo os testes em C++/CLI

Como mencionei, há algumas limitações que você deverá levar em consideração quando misturar código C++ nativo com código C++/CLI. As diferenças são um resultado da diferença no gerenciamento de memória entre as duas bases de código. O C++ nativo usa o operador new do C++ para alocar a memória, e você mesmo fica responsável por liberar essa memória. Depois de alocar uma parte da memória, seus dados sempre ficarão no mesmo local.

Por outro lado, os ponteiros no código C++/CLI têm um comportamento muito diferente por causa do modelo de coleta de lixo herdado do .NET Framework. Você cria novos objetos .NET em C++/CLI usando o operador gcnew em vez do operador new, que retorna um identificador de objeto, e não um ponteiro para o objeto. Os identificadores são basicamente ponteiros para um ponteiro. Quando a coleta de lixo move os objetos gerenciados na memória, ela atualiza os identificadores com a nova localização.

Você precisa ter muito cuidado ao misturar ponteiros gerenciados e nativos. Abordarei algumas dessas diferenças, e fornecerei dicas e truques para obter o máximo dos seus testes em C++/CLI para objetos C++ nativos.

Digamos que um método que você quer testar retorna um ponteiro para uma cadeia de caracteres. No C++, você pode representar o ponteiro da cadeia de caracteres com LPCTSTR. Mas uma cadeia de caracteres do .NET é representada por String^ em C++/CLI. O circunflexo depois do nome da classe significa um identificador para um objeto gerenciado.

Aqui está um exemplo de como você pode testar o valor de uma cadeia de caracteres retornada por uma chamada de método.

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

A última linha contém todos os detalhes. Há um método AreEqual que aceita cadeias de caracteres gerenciadas, mas não há um método correspondente para as cadeias de caracteres C++ nativas. Como resultado, você precisa usar cadeias de caracteres gerenciadas. O primeiro parâmetro para o método AreEqual é uma cadeia de caracteres gerenciada, por isso, na verdade, ela é uma cadeia de caracteres Unicode, mesmo que não esteja marcada como uma cadeia de caracteres Unicode usando _T ou L, por exemplo.

A classe String tem um construtor que aceita uma cadeia de caracteres C++, por isso, você pode criar uma nova cadeia de caracteres gerenciada que conterá o valor real do método que você estiver testando, ponto em que o AreEqual garante que ambos têm o mesmo valor.

A classe Assert tem dois métodos que podem parecer muito atraentes: IsNull e IsNotNull. No entanto, o parâmetro para esses métodos é um identificador, e não um ponteiro de objeto, o que significa que você pode usá-los apenas com objetos gerenciados. Em vez disso, você pode usar o método IsTrue, assim:

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

Você chegará ao mesmo resultado, mas com um pouco mais de códigos. Adicionei um comentário para que a expectativa seja clara na mensagem que aparece na janela de resultados do teste, que pode ser vista na Figura 5.

Test Results Showing the Additional Comment in the Error Message

Figura 5 Resultados do teste mostrando o comentário adicional na mensagem de erro

Compartilhamento de código de configuração e desmontagem

Seu código de teste deve ser tratado como um código de produção. Em outras palavras, você deve refatorar os testes tanto quanto o código de produção para que o código de teste permaneça mais fácil de manter. Em algum momento, você pode ter alguns códigos de configuração e desmontagem comuns para todos os métodos de teste em uma classe de teste. Você pode designar um método que será executado antes de cada teste, bem como um método que é executado depois de cada teste (você pode ter apenas um deles, ambos ou nenhum).

O atributo TestInitialize marca um método que será executado antes de cada método de teste em sua classe de teste. Da mesma forma, o TestCleanup marca um método que é executado depois de cada método de teste em sua classe de teste. Eis um exemplo:

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

MyClass *m_pInstance;

Primeiro, observe que usei um ponteiro simples para a classe para m_pInstance. Por que não usei unique_ptr para gerenciar o tempo de vida?

A resposta, mais uma vez, está relacionada com o fato de misturar C++ nativo e C++/CLI. As variáveis de instância em C++/CLI são parte de um objeto gerenciado e, portanto, apenas podem ser identificadores para objetos gerenciados, ponteiros para objetos nativos ou tipos de valor. Você deve voltar para os princípios básicos de new e delete para gerenciar o tempo de vida das suas instâncias C++ nativas.

Uso de ponteiros para variáveis de instância

Se você estiver usando COM, pode se deparar com uma situação em que possa querer escrever algo mais ou menos parecido com isto:

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

IUnknown *m_pUnk;

Esse código não compilará, e produzirá uma mensagem de erro como esta:

cannot convert parameter 1 from 'cli::interior_ptr<Type>' to 'IUnknown **'

O endereço de uma variável de instância C++/CLI tem o tipo interior_ptr<IUnknown *> neste caso, que não é um tipo compatível com o código C++ nativo. “Por quê?”, você me pergunta. Eu só queria um ponteiro.

A classe de teste é uma classe gerenciada, por isso, as instâncias desta classe podem ser movidas na memória pelo coletor de lixo. Portanto, se você tivesse um ponteiro para uma variável de instância, e depois o objeto fosse movido, o ponteiro se tornaria inválido.

Você pode bloquear o objeto durante sua chamada nativa, desta maneira:

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

A primeira linha bloqueia a instância até que a variável saia do escopo, o que, em seguida, permite que você passe um ponteiro para a variável de instância para um C++ nativo, mesmo que essa variável esteja contida em uma classe de teste gerenciada.

Como escrever código testável

No início deste artigo, mencionei a importância de escrever um código testável. Usei o TDD para garantir que o meu código é testável, mas alguns desenvolvedores preferem escrever testes logo depois de terem escrito seus códigos. Em ambos os casos, é importante não pensar apenas nos testes de unidade, mas na pilha completa de testes.

Mike Cohn, um autor conhecido e produtivo no Agile, desenhou uma pirâmide de automação de teste que fornece uma ideia dos tipos e quantidades de testes que devem existir em cada nível. Os desenvolvedores devem escrever todos ou grande parte dos testes de unidade e de componente, e talvez alguns testes de integração. Para obter detalhes sobre essa pirâmide de teste, leia a postagem “A camada esquecida da pirâmide de automação de teste” de Cohn no blog (bit.ly/eRZU2p).

Os testadores geralmente são responsáveis por escrever testes de aceitação e de IU. Eles também são chamados de testes de ponta a ponta, ou E2E. Na pirâmide de Cohn, o triângulo de IU é o menor comparado com as áreas para os outros tipos de testes. A ideia é que o ideal seria escrever o menor número possível de testes de UI automatizados. Os testes de UI automatizados tendem a ser frágeis e caros para escrever e manter. Pequenas alterações na interface do usuário podem facilmente quebrar os testes de IU.

Se o seu código não for escrito para ser testável, você pode facilmente acabar tendo uma pirâmide invertida, na qual grande parte dos testes automatizados são testes de IU. Essa é uma situação ruim, mas o ponto principal é que cabe ao desenvolvedor garantir que os testadores possam escrever testes de integração e aceitação abaixo da interface do usuário.

Além disso, por algum motivo, a maioria dos testadores com os quais me deparei se sente muito confortável em escrever testes em C#, mas evita escrever testes em C++. Como resultado, nossa equipe precisou de uma ponte entre o código C++ em teste e os testes automatizados. A ponte tem a forma de acessórios, que são classes C++/CLI que parecem ser como qualquer outra classe gerenciada para o código C#.

Criação de acessórios C# para C++

As técnicas aqui não são muito diferentes das que abordei para escrever testes em C++/CLI. Ambas usam o mesmo tipo de código de modo misto. A diferença é a forma como elas são usadas no final.

A primeira etapa é criar um novo projeto que conterá seus acessórios:

  1. Clique com o botão direito do mouse no nó solução em Solution Explorer, em Add (Adicionar) e, em seguida, em New Project (Novo Projeto).
  2. Em Other Languages (Outras Linguagens), Visual C++, CLR, clique em Class Library (Biblioteca de Classes).
  3. Digite o nome a ser usado para este projeto e clique em OK.
  4. Repita as etapas de criação de um projeto de teste para adicionar uma referência e os arquivos de inclusão.

A classe de acessório parecerá um pouco com a classe de teste, mas sem os vários atributos (veja a Figura 6).

Figura 6 Acessório de teste C# para 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 não há arquivos de cabeçalho! Esse é um dos meus recursos favoritos no C++/CLI. Como essa biblioteca de classes cria um assembly gerenciado, as informações sobre as classes são armazenadas como informações tipo .NET, por isso, arquivos de cabeçalho não são necessários.

Essa classe também contém um destruidor e um finalizador. O destruidor aqui não é o destruidor de verdade. Em vez disso, o compilador reescreve o destruidor para uma implementação do método Dispose na interface IDisposable. Qualquer classe C++/CLI que tenha um destruidor, portanto, implementa a interface IDisposable.

O método !MyCodeFixture é o finalizador, que é chamado pelo coletor de lixo quando decide liberar esse objeto, a menos que você tenha chamado o método Dispose anteriormente. Você pode empregar uma instrução using para controlar o tempo de vida do seu objeto C++ nativo incorporado, ou pode deixar que o coletor de lixo gerencie o tempo de vida. Você encontra mais detalhes sobre esse comportamento no artigo da Biblioteca MSDN, “Alterações na semântica do destruidor”, em bit.ly/kW8knr.

Depois que tiver uma classe de acessório C++/CLI, você poderá escrever um teste de unidade em C# que se parece um pouco com a Figura 7.

Figura 7 Um sistema de teste de unidade em 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);
      }
    }
  }
}

Gosto de empregar uma instrução using para controlar explicitamente o tempo de vida do objeto de acessório, em vez de contar com o coletor de lixo. Isso é particularmente importante em métodos de teste para garantir que os testes não interajam com outros testes.

Captura e geração de relatórios de cobertura de código

A última parte que descrevi no início deste artigo foi a cobertura de código. A meta da minha equipe é que a cobertura de código seja capturada automaticamente pelo servidor de compilação, seja publicada no TFS e fique facilmente disponível para todos.

Minha primeira etapa foi descobrir como capturar a cobertura de código C++ de testes em execução. Em uma busca na Web, encontrei uma postagem informativa no blog de Emil Gustafsson, com o título “Relatórios de cobertura de código C++ nativo usando o Visual Studio 2008 Team System” (bit.ly/eJ5cqv). Essa postagem mostra as etapas necessárias para capturar informações de cobertura de código. Transformei isso em um arquivo CMD que posso executar a qualquer momento em minha máquina de desenvolvimento para capturar informações 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

Você deve substituir Tests.dll pelo nome real do seu DLL que contém testes. Você também precisará preparar os seus DLLs para que eles sejam instrumentados:

  1. Clique com o botão direito do mouse no projeto de teste na janela Solution Explorer.
  2. Clique em Properties (Propriedades).
  3. Selecione a configuração de Debug (Depuração).
  4. Expanda Configuration Properties (Propriedades de Configuração) e, em seguida, expanda Linker (Vinculador) e clique em Advanced (Avançado).
  5. Altere a propriedade Profile para Yes (/PROFILE).
  6. Clique em OK.

Essas etapas habilitam a criação de perfil, que deve estar ativada a fim de instrumentar os assemblies para que você possa capturar as informações de cobertura de código.

Recrie seu projeto e execute o arquivo CMD. Isso deve criar um arquivo de cobertura. Carregue esse arquivo de cobertura no Visual Studio para garantir que você poderá capturar a cobertura de código dos seus testes.

A realização dessas etapas no servidor de compilação e a publicação dos resultados no TFS exigem um modelo de compilação personalizado. Os modelos de compilação do TFS são armazenados no controle de versão e pertencem a um projeto de equipe específico. Você encontrará uma pasta chamada BuildProcessTemplates em cada projeto de equipe que provavelmente terá vários modelos de compilação.

Para usar o modelo de compilação personalizado incluído no download, abra a janela Gerenciador do Controle do Código-Fonte. Navegue até a pasta BuildProcessTemplates em seu projeto de equipe e garanta que ela está mapeada para um diretório em seu computador. Copie o arquivo BuildCCTemplate.xaml para esse local mapeado. Adicione esse modelo ao controle do código-fonte e faça o check-in.

É preciso fazer o check-in dos arquivos de modelo antes de poder usá-los nas definições de compilação.

Agora que você já fez o check-in do modelo de compilação, você pode criar uma definição de compilação para executar a cobertura de código. A cobertura de código C++ é coletada usando o comando vsperfmd, conforme mostrado anteriormente. O vsperfmd escuta as informações de cobertura de código para todos os executáveis instrumentados que forem executados enquanto o vsperfcmd estiver em execução. Portanto, o ideal seria não executar outros testes instrumentados ao mesmo tempo. Você também deve garantir que há apenas um agente de compilação em execução na máquina que processará essas execuções de cobertura de código.

Criei uma definição de compilação que seria executada todas as noites. Você pode fazer a mesma coisa seguindo estas etapas:

  1. Na janela do Team Explorer, expanda o nó do seu projeto de equipe.
  2. Clique com o botão direito do mouse em Builds (Compilações), que é um nó em seu projeto de equipe.
  3. Clique em New Build Definition (Nova Definição de Compilação).
  4. Na seção Trigger (Disparador), clique em Schedule (Cronograma) e selecione os dias em que deseja executar a cobertura de código.
  5. Na seção Process (Processo), clique em Show details (Exibir detalhes) na seção chamada Build process template (Criar modelo de processo) no topo e, em seguida, selecione o modelo de compilação no qual você fez o check-in no controle do código-fonte.
  6. Preencha as outras seções necessárias e salve.

Adição de um arquivo de configurações de teste

A definição de compilação também precisará de um arquivo de configurações de teste. Esse é um arquivo XML que lista os DLLs para os quais você deseja capturar e publicar os resultados. Estas são as etapas para configurar esse arquivo para cobertura de código:

  1. Clique duas vezes no arquivo de configurações Local.test para abrir a caixa de diálogo Test Settings (Configurações de Teste).
  2. Clique em Data and Diagnostics (Dados e Diagnóstico) na lista à esquerda.
  3. Clique em Code Coverage (Cobertura de Código) e marque a caixa de seleção.
  4. Clique no botão Configure (Configurar) acima da lista.
  5. Marque a caixa ao lado do seu DLL que contém os seus testes (que também contém o código que os testes estão testando).
  6. Desmarque Instrument assemblies in place (Instrumentar os assemblies no local), pois a definição de compilação cuidará disso.
  7. Clique em OK, Apply (Aplicar) e, em seguida, Close (Fechar).

Se você quiser criar mais de uma solução, ou se tiver mais de um projeto de teste, precisará de uma cópia do arquivo de configurações de teste que inclui os nomes de todos os assemblies que devem ser monitorados para a cobertura de código.

Para isso, copie o arquivo de configurações de teste para a raiz de sua ramificação e dê a ele um nome descritivo, como CC.testsettings. Edite o XML. O arquivo conterá pelo menos um elemento CodeCoverageItem das etapas anteriores. Você deve adicionar uma entrada para cada DLL que deseja capturar. Observe que os caminhos são relativos ao local do arquivo de projeto, e não ao local do arquivo de configurações de teste. Faça o check-in desse arquivo no controle do código-fonte.

Por fim, você precisa modificar a definição de compilação para usar esse arquivo de configurações de teste:

  1. Na janela do Team Explorer, expanda o nó do seu projeto de equipe e, em seguida, expanda Builds (Compilações).
  2. Clique com o botão direito do mouse na definição de compilação que você criou antes.
  3. Clique em Edit Build Definition (Editar Definição de Compilação).
  4. Na seção Process (Processo), expanda Automated Tests (Testes Automatizados), Test Assembly (Assembly de Teste) e, em seguida, clique em TestSettings File. Clique no botão ... e selecione o arquivo de configurações de teste criado anteriormente.
  5. Salve as suas alterações.

Você pode testar essa definição de compilação clicando com o botão direito do mouse e selecionando Queue New Build (Enfileirar Nova Compilação) para iniciar uma nova compilação imediatamente.

Geração de relatórios de cobertura de código

Criei um relatório personalizado do SQL Server Reporting Services que exibe a cobertura de código, conforme mostrado na Figura 8 (borrei os nomes reais dos projetos para proteger os envolvidos). Esse relatório usa uma consulta SQL para ler os dados no depósito do TFS e exibe os resultados combinados para os códigos C++ e C#.

The Code-Coverage Report

Figura 8 O relatório de cobertura de código

Não abordarei todos os detalhes sobre como esse relatório funciona, mas há alguns aspectos que gostaria de mencionar. O banco de dados contém informações em excesso da cobertura de código C++ por dois motivos: o código do método de teste é incluído nos resultados, e as bibliotecas C++ padrão (que estão nos arquivos de cabeçalho) são incluídas nos resultados.

Adicionei um código na consulta SQL que filtra esses dados extras. Se você observar o SQL no relatório, verá isto:

    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::%'

Essas linhas excluem resultados de cobertura de código para namespaces específicos (std, stdext e anonymous) e algumas classes fornecidas com o Visual C++ (_bstr_t e _com_error), bem como qualquer código que esteja dentro de um namespace que termine com Tests.

O último, que exclui namespaces que terminam com Tests, exclui qualquer método que esteja em classes de teste. Quando você cria um novo projeto de teste, como o nome do projeto termina com Tests, todas as classes de teste por padrão estarão dentro de um namespace que termina com Tests. Você pode adicionar outras classes ou namespaces aqui que deseja excluir.

Isso é apenas uma pequena amostra do que você pode fazer — acompanhe o nosso progresso em meu blog blogs.msdn.com/b/jsocha.           

John Socha-Leialoha é desenvolvedor no grupo Management Platforms & Service Delivery da Microsoft. Suas realizações anteriores incluem ter escrito o programa Norton Commander (em C e assembler) e o livro “Linguagem Assembly para IBM PC” (Editora Campus, 1987).

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Rong Lu