Revista MSDN Magazine Edição 11 - Automação de Teste para Apps Web ASP.NET com SSL

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (1)  (0)

msdn11_capa.jpg

Clique aqui para ler todos os artigos desta edição

 

Automação de Teste para Apps Web ASP.NET com SSL

por James McCaffrey

 

Se você estiver criptografando dados de usuário com SSL (Secure Sockets Layer) sobre HTTP e quiser testar seus aplicativos Web programaticamente, descobrirá que as técnicas não são amplamente conhecidas. Na coluna deste mês, eu mostrarei a você como configurar um servidor SSL de teste e escrever uma automação de teste que verifica a funcionalidade de um aplicativo Web simples mas representativo.

O ambiente Microsoft® .NET fornece ferramentas poderosas para testar os aplicativos Web ASP.NET que utilizam o mecanismo de segurança do SSL. Para ilustrar o uso dessas ferramentas, configurarei um servidor SSL de teste e demonstrarei um programa pequeno (porém poderoso) que testa automaticamente um aplicativo Web de exemplo que usa HTTPS. Embora as técnicas individuais sejam elegantes e documentadas, nas conversas com meus colegas descobri que o processo geral de se testar aplicativos Web em SSL é relativamente pouco conhecido. A melhor maneira de mostrar a você o que estou fazendo é por meio de duas figuras. A Figura 1 mostra o aplicativo Web ASP.NET que pretendo testar (simples, mas representativo).

 

image001.gif

Figura 1 Um aplicativo Web ASP.NET

 

Observe que estou usando uma conexão SSL porque estou enviando informações confidenciais de cartão de crédito pela Internet. (Repare o protocolo "https://" na barra de endereços e o ícone de cadeado na barra de status.)

Agora, apenas imagine como seria testar manualmente o aplicativo. Você precisaria informar centenas ou até mesmo milhares de nomes, quantidades e números de cartão de crédito na página Web, examinar visualmente cada código de confirmação resultante, comparar manualmente cada código com uma lista de resultados esperados para determinar um resultado de êxito ou de falha e, em seguida, gravar os resultados em algum form como uma planilha do Excel ou um documento de texto. Todo esse processo demanda tempo, é propenso a erros, é ineficaz e claramente maçante.

Uma abordagem muito melhor é utilizar os recursos avançados do .NET Framework para escrever uma automação que envie programaticamente os dados do teste por meio de SSL e, em seguida, examinar a stream de resposta em busca de um código de confirmação esperado. O aplicativo Console mostrado na Figura 2 faz exatamente isso.

 

image002.gif

Figura 2 O Aplicativo de Teste

 

Como você pode ver, o teste automatizado 001 corresponde ao teste manual mostrado na Figura 1. O sobrenome "Smith", a quantidade "3" e o número de cartão de crédito "1234 5678 9012" são criptografados e postados por meio de programação para o aplicativo Web que utiliza HTTP com SSL. O programa de teste recupera a stream de resposta HTTP e faz uma busca por "C3-57-ED-DA-8B". Nesse caso, o código de confirmação esperado foi encontrado na stream de resposta, e a automação do teste registra um resultado PASS. A seguir, apresentarei e descreverei o programa de teste que gerou a saída mostrada na Figura 2, demonstrarei como configurar um servidor de teste que aceite solicitações de SSL e explicarei como você pode estender as técnicas apresentadas de acordo com as suas próprias necessidades.

Antes de passarmos para o programa de automação de teste, vamos dar uma rápida olhada no aplicativo Web de exemplo. Como você pode ver na Figura 1, existem três controles TextBox; eu aceitei as Ids padrão do Visual Studio® .NET para TextBox1, TextBox2 e TextBox3, de modo a manter o sobrenome do usuário, a quantidade de widget e o número do cartão de crédito, respectivamente. O aplicativo exibe sua mensagem no controle Label5. Precisarei ter essas informações à mão quando escrever a automação do teste. Também precisarei saber como o código de confirmação da ordem é gerado, a fim de que eu possa determinar os resultados esperados para meus testes. Veja a seguir o cerne do código do aplicativo Web que estará sendo testado:

 

if (TextBox3.Text.Length == 0)

  Label5.Text = "Please enter credit card number";

else

{

  byte[] input = Encoding.Unicode.GetBytes(TextBox3.Text);

  byte[] hashed;         

  using(MD5 m = new MD5CryptoServiceProvider())

  {

    hashed = m.ComputeHash(input);

  }

  Label5.Text = "Thank you. Your confirmation code is " +

  BitConverter.ToString(hashed).Substring(0,14);

}

 

Para simular a geração de um código de confirmação, eu apenas utilizo o número de cartão de crédito inserido pelo usuário, produzo um hash MD5 a partir dele e capturo os 14 caracteres iniciais do hash. Em um sistema de produção real, é provável que você queira gerar um código de confirmação de maneira mais complexa. Nesse caso, seria complicado determinar o resultado esperado. No entanto, uma estratégia que você definitivamente não deve usar é determinar o resultado esperado por meio de uma chamada ao aplicativo sob teste. Isso destruiria a validade de seu teste porque você só estaria verificando se a sua automação de teste retorna o mesmo resultado de seu aplicativo.

 

O Programa de Automação de Teste

O programa de automação de teste é surpreendentemente simples. O código completo é apresentado na Listagem 1. Embora as técnicas exigidas para postar dados programaticamente para um aplicativo Web ASP.NET estejam documentadas na MSDN® Library, existem vários truques com os quais você deve tomar cuidado.

 

Listagem 1 O Programa de Automação de Teste

using System;

using System.Web;  // para classe HttpUtility

using System.Text; // para classes Encoding e StringBuilder

using System.Net;  // para classes HttpWebRequest

using System.IO;   // para classes StreamReader

 

namespace Run

{

  class Class1

  {

    [STAThread]

    static void Main(string [] args)

    {

      Console.WriteLine("\nStarting Test Run\n");

 

      string url = "https://localhost/LitwareOrder/Order.aspx";

      string viewstate =

        HttpUtility.UrlEncode(

        "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA==");

      string line;

      string[] tokens;

      StringBuilder data = new StringBuilder();

      byte[] buffer;

 

      using(FileStream fs = new FileStream(args[0], FileMode.Open))

      {

        StreamReader tc = new StreamReader(fs);

        while ((line = tc.ReadLine()) != null)

        {

          tokens = line.Split(':');

          data.Length = 0;

          data.Append("TextBox1=" + tokens[1]);  // Last name

          data.Append("&TextBox2=" + tokens[2]); // Quantity

          data.Append("&TextBox3=" + tokens[3]); // Credit card number

          data.Append("&Button1=clicked");

          data.Append("&__VIEWSTATE=" + viewstate);

 

          buffer = Encoding.UTF8.GetBytes(data.ToString());

 

          HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);

          req.Method = "POST";

          req.ContentType = "application/x-www-form-urlencoded";

          req.ContentLength = buffer.Length;

          req.CookieContainer = new CookieContainer(); // enable cookies

 

          using (Stream reqst = req.GetRequestStream())

          {

            reqst.Write(buffer, 0, buffer.Length);

          }

 

          Console.WriteLine("====================");

          Console.WriteLine("\nTest case ID = " + tokens[0] );

          Console.WriteLine("Posting " + tokens[1] + ", " + tokens[2] +

                          "," + tokens[3] );

          Console.WriteLine("To " + url);

          Console.WriteLine("Looking for " + tokens[4] );

 

          using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())

          {

            string result;

            using(Stream resst = res.GetResponseStream())

            {

              result = new StreamReader(resst).ReadToEnd();

            }

            //Console.WriteLine(result);

 

            if (result.IndexOf(tokens[4]) >= 0)

              Console.WriteLine("PASS");

            else

              Console.WriteLine("FAIL");

              Console.WriteLine("");

          }

        } // loop while

 

        Console.WriteLine("====================");

        Console.WriteLine("Done");

      }

      Console.ReadLine();

    } // Main()

  } // classe Class1

} // namespace Run

 

Decidi escrever um aplicativo de console C# como meu programa de teste. Em geral, recomenda-se usar a mesma linguagem do sistema sob teste para a automação de seu teste. No entanto, o design bem-programado e a integração do ambiente .NET significam que você pode usar com segurança o Visual Basic® .NET ou qualquer outra linguagem compatível com .NET. Geralmente, os aplicativos de console são programas mais apropriados para a automação do teste. Embora um programa de automação de teste ágil e com uma ótima interface de usuário cause uma excelente impressão em seus usuários, a automação de teste é uma ferramenta, e não uma vitrine pessoal, e os aplicativos de console podem ser integrados em um sistema de build mais facilmente do que um aplicativo GUI.

A estrutura geral da automação do teste é bastante simples. Eu armazeno os dados do teste em um arquivo de texto simples. Cada linha de dados representa um único teste. Veja a seguir o arquivo de teste que eu usei:

 

001:Smith:3:1234 5678 9012:C3-57-ED-DA-8B

002:Baker:2:1111 2222 3333:CE-81-8C-2F-94

003:Gates:9:9999 9999 9999:95-D6-05-31-8A

 

As informações são separadas por um sinal de dois-pontos (:). Eu poderia ter usado qualquer caractere como um delimitador, mas é importante evitar caracteres que apareçam nos dados do teste real. O primeiro campo contém uma ID de teste, o segundo campo contém o sobrenome, o terceiro campo contém a quantidade de widgets, o quarto contém o número do cartão de crédito, e o quinto contém o código de confirmação esperado. Se você não quiser usar um arquivo de texto, os arquivos XML e as tabelas SQL constituem boas alternativas para o armazenamento de testes.

A estrutura básica de minha automação de teste está ligada à estrutura de meu arquivo de teste. No pseudocódigo, o plano aparece assim:

 

loop

  read a test case line

  parse out test case data

  build up data to post to application

  convert post data to a byte array

  post the data

  retrieve the response stream

  if response stream contains expected confirmation code

    log "pass" result

  else

    log "fail" result

end loop

 

Eu começo declarando os namespaces que usarei. Isso evita que eu tenha de qualificar totalmente cada classe e objeto .NET e também documenta a funcionalidade a ser empregada pelo programa de automação de teste:

 

using System;

using System.Web;

using System.Text;

using System.Net; 

using System.IO;  

 

O namespace System.Web contém a classe HttpUtility que eu usarei para converter caracteres especiais em seqüências de escape. Como um Console Application padrão não faz referência a nenhuma System.Web.dll (que aloja essa classe), precisei adicionar manualmente ao arquivo uma referência ao projeto. O namespace System.Text possui uma classe Encoding que eu usarei para executar a manipulação do array de bytes. O namespace System.Net contém a classe HttpWebRequest, que é a classe fundamental a ser usada para postar os dados no aplicativo Web ASP.NET. Eu optei por usar esse namespace System.IO porque farei uso de streams de dados para a resposta HTTP sobre o SSL e porque precisarei que ele leia os dados do teste a partir de um arquivo texto. Observe que o uso de diretivas permite o uso de tipos em um namespace para que você não precise qualificar o uso de tipos naquele namespace.

Em seguida, após exibir uma breve mensagem para o shell de comando, declaro as variáveis chave que a automação do teste usará:

 

string url = "https://localhost/LitwareOrder/Order.aspx";

string viewstate = HttpUtility.UrlEncode(

    "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA==");

string line;

string[] tokens;

StringBuilder data = new StringBuidler();

byte[] buffer;

string proxy = null;

 

A finalidade da maioria dessas variáveis provavelmente será óbvia a partir de seus próprios nomes, mas talvez a variável viewstate seja nova para você, por isso vou descrevê-la rapidamente. Agora, abro o arquivo de teste especificado na linha de comandos e o leio linha por linha:

 

using(FileStream fs = new FileStream(args[0], FileMode.Open))

{

  StreamReader tc = new StreamReader(fs);

  while ((line = tc.ReadLine()) != null)

  {

    // analise a linha, poste os dados, obtenha resposta

    // determine se pass ou fail, registre o resultado

  }

}

 

Existem muitas maneiras alternativas de projetar a automação, mas essa estrutura simples provou ser sólida em vários projetos maiores. A próxima etapa consiste em analisar cada campo dos dados do teste e criar uma string de dados que contenha pares de nome-valor:

 

tokens = line.Split(':');

data.Length = 0;

data.Append("TextBox1=" + tokens[1]);   // Last name

data.Append("&TextBox2=" + tokens[2]); // Quantity

data.Append("&TextBox3=" + tokens[3]); // Credit card number

data.Append("&Button1=clicked");

data.Append("&__VIEWSTATE=" + viewstate);

 

Eu uso o método String.Split para dividir a linha do teste e armazenar cada campo no array de tokens. O ID do teste é armazenado em tokens[0], o sobrenome do usuário é armazenado em tokens[1], a quantidade de widgets é armazenada em tokens[2] e o número do cartão de crédito é armazenado em tokens[3]. Para mais clareza, eu poderia ter criado variáveis de string adicionais com nomes descritivos como "caseID" e "lastName" e, em seguida, copiado nelas os valores do arrays de tokens, conforme mostrado neste exemplo:

 

caseID = tokens[0];

lastName = tokens[1];

// etc.

 

No entanto, eu queria utilizar o mínimo possível de variáveis. Os servidores Web tradicionais esperam dados POST em pares de nome-valor (name-value) conectados por um sinal de E commercial (&), como se vê nesta linha:

 

lastName=Smith&quantity=3&creditCardNo=123456789012

 

No entanto, o ASP.NET amplia essa idéia. Nesse exemplo, há cinco pares de nome-valor (name-value). O primeiro par, TextBox1=tokens[1] é o que você deve esperar. Ele atribui o valor do sobrenome do teste atual (armazenado em tokens[1]) ao controle com um atributo de id "TextBox1". O segundo par, TextBox2=tokens[2] e o terceiro par, TextBox3=tokens[3], também deverão fazer sentido para você ao atribuir a quantidade de widgets e o número de cartão de crédito, respectivamente. O próximo par, "Button1=clicked", provavelmente não é o que você esperava (se você tem experiência em postar dados para páginas ASP tradicionais). Como Button1 é um controle no lado servidor, eu preciso postá-lo para manter o valor de ViewState sincronizado, conforme explicarei mais adiante. Atribuir qualquer valor a ele não terá nenhum efeito, por isso eu poderia ter escrito "Button1=" apenas. Eu gosto de usar um valor do tipo "clicked" porque ele torna o código mais legível. O quinto par de nome-valor é o par __VIEWSTATE (observe os dois sublinhados); essa é a verdadeira chave para postar programaticamente em servidores ASP.NET.

O que é esse valor ViewState? Embora o HTTP seja um protocolo stateless — cada par Request-Response é uma transação isolada — o ASP.NET trabalha nos bastidores para criar um ambiente stateful. Uma das maneiras que o ASP.NET usa para isso é através de uma entrada de HTML oculta denominada __VIEWSTATE. Ela consiste em um valor de string codificado como Base64 que representa o estado da página quando ela foi processada pela última vez pelo servidor. Dessa forma, as páginas podem manter o estado, retendo os valores entre sucessivas chamadas. Para postar corretamente as informações para um aplicativo ASP.NET, eu preciso enviar o valor da ViewState para o servidor. Lembre-se de que eu defino o valor de viewstate como:

 

string viewstate = HttpUtility.UrlEncode(

    "dDw0MDIxOTUwNDQ7Oz6E/7ailqx8X9zCUfpbWTPybfS4MA==");

 

De onde veio esse valor? A maneira mais fácil de obter o valor da ViewState inicial de um aplicativo Web é simplesmente acionar o Microsoft Internet Explorer, obter a página e acessar View | Source na barra de menus. É importante que você obtenha o valor da ViewState inicial porque, se recarregar a página, ele será alterado e seu programa de postagem programática gerará um erro de servidor. O valor da ViewState bruta precisa ser processado no método UrlEncode. O método UrlEncode converte os caracteres inválidos de um URL em seqüências de escape. Por exemplo, os caracteres "=" são convertidos em seqüências %3D.

Uma vez obtido o valor de ViewState, eu posso construir a string de dados completa que irei postar no aplicativo Web. Em seguida, precisarei converter essa string em um array de bytes porque o método que irá postar os dados requer que estes sejam armazenados como bytes:

 

buffer = Encoding.UTF8.GetBytes(data);

 

O método GetBytes é um membro da classe Encoding do namespace System.Text. Além da propriedade UTF8, existem também as propriedades ASCII, Unicode e UTF7. Agora, criarei uma instância de um objeto HttpWebRequest e atribuirei valores à suas propriedades:

 

HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);

req.Method = "POST";

req.ContentType = "application/x-www-form-urlencoded";

req.ContentLength = buffer.Length;

req.Proxy = new WebProxy(proxy, true);

req.CookieContainer = new CookieContainer();

 

Observe que, seguindo o padrão de WebRequest, utilizo um método Create explícito, em vez de chamar um construtor com a nova palavra-chave. Uso o método POST porque estou enviando dados do formulário. Defino a propriedade ContentType como "application/x-www-form-urlencoded". Ela é um tipo MIME que age como uma string mágica, que diz ao servidor ASP.NET que ele deve esperar dados de formulário. Agora, eu defino a propriedade ContentLength como o número de bytes de dados de postagem que armazenei anteriormente no buffer do array de dados.

Nesse exemplo, poderia ter deixado de fora a propriedade Proxy se não estivesse enviando dados através de uma máquina de servidor proxy. O parâmetro Boolean true no construtor WebProxy significa ignorar o proxy dos endereços locais. A atribuição de um valor à propriedade CookieContainer é necessária para que os cookies sejam retornados na propriedade Cookies do HttpWebRequest retornado por GetResponse. Observe que eu atribuo um objeto CookieContainer vazio. Esse é um dos pequenos detalhes que me causaram muitos problemas quando eu estava desvendando essa técnica.

Antes de postar a solicitação para o aplicativo ASP.NET, é necessário adicionar os dados de postagem ao objeto Request, desta maneira:

 

using (Stream reqst = req.GetRequestStream())

{

  reqst.Write(buffer, 0, buffer.Length);

}

 

Em seguida, depois de imprimir algumas informações sobre o que está sendo postado, eu recupero a stream de resposta resultante no servidor:

 

using(HttpWebResponse res = (HttpWebResponse)req.GetResponse())

{

  string result;

  using(Stream resst = res.GetResponseStream())

  {

    result = new StreamReader(resst).ReadToEnd();

  }

  //Console.WriteLine(result);

}

 

Você deve ter esperado (assim como eu) que eu enviasse explicitamente a solicitação com uma instrução como req.Send(data). No entanto, o uso de HttpWebRequest.GetRequestStream na verdade abre a conexão para o servidor, e HttpWebRequest.GetResponse recupera o objeto HttpWebResponse que representa a resposta do servidor (se GetRequestStream não fosse usado, GetResponse também estabeleceria a conexão com o servidor.) Capturo a stream de resposta usando ReadToEnd e salvo-a em uma variável de string denominada "result". Você também pode ler a resposta linha por linha usando o método ReadLine. Observe que comentei uma instrução que exibiria a stream de resposta completa para o shell de comando. Caso não tenha experiência nesse tipo de programação, você achará mais instrutivo retirar o comentário dessa linha, a fim de que possa ver toda a stream de resposta.

Por fim, examino a stream de resposta para ver se ela contém o código de confirmação esperado (stored in tokens[4]):

 

if (result.IndexOf(tokens[4]) >= 0)

  Console.WriteLine("PASS");

else

  Console.WriteLine("FAIL");

 

Se encontrar o valor esperado, registro um resultado PASS para o shell. Claro, você poderia gravar os resultados do teste em um arquivo de texto, em um arquivo XML ou em uma tabela SQL, se preferir.

 

Configurando um Servidor SSL de Teste

Até pouco tempo atrás, era um verdadeiro fardo configurar um servidor Web de teste com SSL ativado. Você pode adquirir um certificado SSL "real" de um dos vários provedores, mas isso toma tempo e dinheiro. Outra opção é gerar um certificado SSL auto-assinado por meio do utilitário makecert.exe que é fornecido como parte do .NET Framework Tools e, em seguida, instalá-lo em seu servidor Web. No entanto, existe uma maneira muito mais simples.

O IIS 6.0 Resource Kit (disponível para download em Windows Deployment and Resource Kits http://www.microsoft.com/windows/reskits/) contém diversas ferramentas valiosas, incluindo uma — selfssl.exe — que facilita a criação e a instalação de um certificado SSL auto-assinado para fins de teste. A Figura 3 mostra exatamente como fiz isso.

 image003.gif

Figura 3 Criação de um Certificado Auto-Assinado

 

A chave é usar a switch /T para que o browser local confie no certificado e também usar a switch /N para especificar "localhost" como o nome Common. Surpreendentemente, isso é tudo de que você precisa para efetuar testes com HTTPS diretamente no servidor Web. Caso queira testar HTTP com SSL a partir de uma máquina-cliente remota, da primeira vez que navegar manualmente até o servidor de teste você receberá uma caixa de diálogo do Security Alert que lhe perguntará se deseja prosseguir. Se você clicar no botão View Certificate e, depois, no botão Install Certificate, será exibido um assistente. Se aceitar todos os padrões do assistente, quando você terminar de instalar o certificado o cliente poderá acessar o servidor de teste sem receber a caixa de diálogo de aviso, e a automação de seu teste será executada no cliente.

Embora a ferramenta selfssl.exe faça parte do IIS 6.0 Resource Kit e não suporte explicitamente versões mais antigas do IIS, meus colegas e eu já a utilizamos sem problemas no IIS 5.0. Também já usei a ferramenta makecert.exe para gerar um certificado x.509 auto-assinado que pode ser usado para testes. A MSDN Library oferece instruções sobre a ferramenta makecert.exe em Certificate Creation Tool, mas o uso da ferramenta selfssl.exe é mais fácil.

Quando terminar os testes com seu certificado SSL auto-assinado, você desejará removê-lo para se prevenir contra possíveis efeitos de interação em seu servidor de testes. A maneira mais fácil de remover o certificado é por meio da MMC (Microsoft Management Console). Acione a MMC e adicione o snap-in Certificates de uma Computer Account para gerenciar o Local Computer. Agora, expanda o armazenamento de Certificates e, em seguida, expanda a pasta Personal. Uma vez selecionada a pasta Certificates, o certificado auto-assinado será exibido e você poderá excluí-lo.

 

Aprofundando Seu Trabalho

Existem várias maneiras de estender as técnicas apresentadas. Eu incluí algumas informações hardcode (por exemplo, o URL de teste) que você desejará extrair, e você provavelmente achará útil criar parâmetros para tornar seu sistema de teste mais flexível. Uma extensão interessante seria determinar programaticamente o valor da ViewState inicial. Lembre-se de que acionei manualmente o browser, depois visualizei a fonte para determinar o valor da ViewState inicial e, em seguida, incluí em hardcode esse valor na automação. Isso funciona, mas sempre que houver uma alteração no código-base do aplicativo Web, você precisará obter o novo valor da ViewState. Uma abordagem melhor seria usar a classe WebClient a partir do namespace System.Net para solicitar programaticamente a página da aplicação Web inicial, analisar a stream de resposta do valor __VIEWSTATE e atribuir esse valor à variável viewstate. Isso adiciona uma rota extra de ida e volta da resposta/solicitação a cada chamada de teste, mas aumenta incrivelmente a flexibilidade de sua automação de teste.

A técnica apresentada complementa satisfatoriamente o teste que você pode executar com a ferramenta ACT (Microsoft Application Center Test). O ACT é projetado para pressionar servidores Web e analisar a performance dos aplicativos Web para que você possa lidar com problemas de escalabilidade. No entanto, o ACT não foi projetado para lidar com verificações de funcionamento (diferentemente da técnica mostrada aqui, que foi projetada para isso).

Neste artigo, usei a classe HttpWebRequest para postar dados programaticamente para o aplicativo Web ASP.NET sob teste, mas existem várias outras alternativas que você pode usar. Em um nível de abstração mais baixo, é possível postar dados usando a classe Sockets no namespace System.Net.Sockets. Em um nível de abstração um pouco mais alto, você pode usar a classe WebClient no namespace System.Net. Todas as três técnicas funcionam bem, mas tanto eu como a maioria de meus colegas preferimos a classe HttpWebRequest. Contudo, você vai perceber que é tudo uma questão de preferência pessoal e de cenário.

 

Download disponível em www.neoficio.com.br/msdn TestRun0408.exe (133KB)

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?