Páginas

domingo, 9 de junho de 2013

Pex no Visual Studio 2012: Code Digger e TDD com teorias

Uma ferramenta que eu tenho acompanhado a evolução é o Pex (http://research.microsoft.com/en-us/projects/pex/). Trata-se de um projeto conduzido pela Microsoft Research desde os tempos de Visual Studio 2010. Como um subproduto deste projeto nasceu o Moles, um framework de isolamento de dependências, que na versão 2012 do Visual Studio tornou-se o Fakes.

Cheguei a publicar artigos e posts referente a essas ferramentas (Introdução ao framework de testes Microsoft Moles - Artigo Revista . Net Magazine 87, Geração automática de testes numa abordagem TDD - Revista .Net Magazine 90 e "Novidades" no Visual Studio 11 Beta - Microsoft Fakes). Confesso que a falta de novidades no Pex me deixou um pouco desanimado desde então, mas recentemente foi lançada uma primeira extensão dela voltada para o Visual Studio 2012: Code Digger!

Mas o que faz o Pex? Bom, ele é uma ferramenta de análise de código que busca encontrar todos os caminhos lógicos de forma a identificar valores que, quando passados para seus métodos, acabem gerando erros. Para fazer isso, ele analisa todas as possibilidades de instruções condicionais (if/else, while, etc.) dentro do seu código, sugerindo valores de parâmetros que sejam "interessantes". Por esta característica, podemos classificar o Pex como uma ferramenta de testes de caixa branca, ou seja, onde o teste é executado sobre uma implementação conhecida (diferente de testes de caixa preta, onde parte-se da premissa que não se conhece o que tem dentro de um objeto que será testado).

O Code Digger é uma extensão do Visual Studio 2012 que pode ser baixada em http://visualstudiogallery.msdn.microsoft.com/fb5badda-4ea3-4314-a723-a1975cbdabb4. Ele não tem todas as funcionalidades que tínhamos na versão 2010 do Pex, mas já é um começo. Após instalado, ele permite gerar uma tabela contendo os valores de parâmetros e respetivos resultados de determinado método para casos interessantes. Para isso, basta clicar com o botão direito sobre a função que queremos analisar, e selecionar a opção "Generate Inputs / Outputs Table".


Aqui tenho que fazer uma ressalva: na versão atual, o Code Digger dá suporte apenas para projetos do tipo Portable Class Library. Mas entendo que essa limitação logo será revista.

Para explicar, nada melhor que um exemplo. Imagine que estamos construindo um novo algoritmo de criptografia simétrico (que usa uma mesma chave para tanto cifrar quanto decifrar valores). A ideia desse algoritmo é transformar cada letra em um número, e separá-los pelo caractere de pipe ("|"). Ou seja, se tivermos o valor "AB" para criptografar, ele deve ser transformado em "14560|14850". Para não ficar tão fácil de descobrir o texto criptografado, nosso algoritmo permite que seja passada uma chave ("salt") para influenciar no valor final. Por exemplo, se o "salt" for 123, o resultado da criptografia de um caractere qualquer seria "12345"; se o "salt" for 456 o resultado final seria "7894", e assim por diante. Obviamente, se utilizarmos o "salt" errado na hora de descriptografar, não teremos o resultado correto.

A imagem abaixo descreve um pouco melhor esse algoritmo. Supondo o valor "AB" e um "salt" com o valor 224. Multiplicando o valor do caractere A na sua representação decimal (65), temos o valor 14560; somamos então +1 para o "salt", dando 225; multiplicamos o valor de B (66) por 225, dando 14850; somamos então +1 para o "salt"; multiplicamos o próximo caractere  por 226, e assim até terminar.


Para descriptografar, fazemos o processo ao contrário: 14560 dividido por 224 para dar 65; 14850 dividido por 225 para dar 66, etc.

Observação: apesar do cuidado de se utilizar uma chave "salt" para se ter resultados diferentes, a ideia desse algoritmo proposto poderia ser quebrada com um pouco de esforço. É claro que algoritmos simétricos da vida real possuem lógicas mais complexas, mas para o nosso exemplo esta lógica já basta.

Trabalhando numa abordagem TDD, podemos criar os seguintes métodos para começarmos a desenvolver nossa classe, baseado nos cenários que descrevemos:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using PexCodeDiggerExample;

namespace PexCodeDiggerExampleTest
{
    [TestClass]
    public class MySymmetricKeyAlgorithmTest
    {
        [TestMethod]
        public void ShouldCryptText()
        {
            var target = new MySymmetricKeyAlgorithm();
            var result = target.Crypt("AB", 224);
            Assert.AreEqual("14560|14850", result);
        }
        [TestMethod]
        public void ShouldDecryptText()
        {
            var target = new MySymmetricKeyAlgorithm();
            var result = target.Decrypt("14560|14850", 224);
            Assert.AreEqual("AB", result);
        }
    }
}

O primeiro método, ShouldCryptText, verifica se a lógica de criptografar que descrevemos está correto. O segundo, ShouldDecryptText, valida se o texto criptografado consegue retornar ao seu valor original após efetuarmos a descriptografia.

O código que desenvolvemos até agora está abaixo, lembrando que TDD é uma técnica iterativa, onde para cada novo teste temos uma versão diferente do código (até termos a versão final).

using System;
using System.Linq;

namespace PexCodeDiggerExample
{
    public class MySymmetricKeyAlgorithm
    {
        public string Crypt(string plainText, int salt)
        {
            char[] chars = plainText.ToCharArray();
            long[] values = new long[chars.Length];
            for (int i = 0; i < chars.Length; i++)
            {
                values[i] = chars[i] * salt;
                salt+= 1;
            }
            return string.Join("|", values);
        }
        public string Decrypt(string cipherText, int salt)
        {
            string[] values = cipherText.Split("|".ToCharArray(), StringSplitOptions.None);
            char[] chars = new char[values.Length];
            for (int i = 0; i < chars.Length; i++)
            {
                chars[i] = (char)(int.Parse(values[i]) / salt);
                salt+= 1;
            }
            return new string(chars);
        }
    }
}

Até aqui não vimos nada de novo. Vamos utilizar a ferramenta Code Digger em cima de um desses métodos, para vermos quais são os erros que ela retorna para nós. Podemos ver o resultado da análise do método Crypt na imagem abaixo.


Apenas um erro encontrado, onde o problema é simplesmente uma exceção de referência de objeto nulo. Aqui cabem algumas críticas que muitos fazem a respeito desse tipo de ferramenta, quando aplicado em desenvolvimento orientado a testes:

"Ferramentas como o Pex / Code Digger não são de muita serventia, pois basicamente permitem que seja obtida 100% de cobertura de testes dos códigos. As situações de erro não são muito significativas da parte do negócio, limitando-se basicamente a testar valores limites dos parâmetros."

Eu concordo com esta crítica. Mas o problema dela é que considera um uso incorreto deste tipo de ferramenta. Realmente não nos é dada nenhuma vantagem nos testes do ponto de vista de negócio, pois os erros que são pegos se resumem a codificação incorreta, como a do exemplo anterior, que poderia ser tratada com uma verificação de objeto nulo. Mas fica somente nisso.

Uma abordagem melhor para tratarmos o uso desse tipo de ferramenta é criar testes de unidade que ajam como teorias. Partimos então de uma afirmação na qual o Pex / Code Digger deve encontrar um caso negativo para invalidá-la. No momento que ele encontra esta situação, sabemos que nosso código possui um erro. Vejamos o exemplo abaixo (que foi criado como um método qualquer dentro do projeto do próprio algoritmo de criptografia, devido à limitação do Portable Class Library que já comentei; entretanto, esse tipo de código ficaria melhor como um teste dentro de um projeto de testes).

using System.Diagnostics.Contracts;

namespace PexCodeDiggerExample
{
    public class MySymmetricKeyAlgorithmTestTheory
    {
        public void EncipherAndDecipherATextWillResultInTheSameText(string plainText, int salt)
        {
            Contract.Assume(!string.IsNullOrEmpty(plainText));
            var target = new MySymmetricKeyAlgorithm();
            Contract.Assume(salt > 0);
            var cipherText = target.Crypt(plainText, salt);
            var plainTextAgain = target.Decrypt(cipherText, salt);
            Contract.Assert(plainTextAgain == plainText);
        }
    }
}

Nossa teoria aqui é que todo texto que é criptografado com uma determinada chave "salt", ao ser submetido ao processo de decriptação, deve sempre resultar no texto original. Ou seja, se criptografando "XYZ" resultar em "123|456|789", fazer a descriptografia de "123|456|789" deve resultar em "XYZ". Essa é a nossa teoria.

Note que estamos utilizando o Code Contracts (http://research.microsoft.com/en-us/projects/contracts/) para definir quais são as premissas da nossa teoria (texto original não nulo e a chave "salt" maior que zero) e também a forma como confirmamos a nossa teoria (valor descriptografado sendo igual ao original). Aqui vale citar que o Pex / Code Digger é um projeto bem "intímo" do Code Contracts, o que nos permite utilizá-lo para controlar a forma com que a análise de código leva o nosso código em consideração.

O resultado da execução do Code Digger, na imagem abaixo, mostra alguns erros que foram identificados no nosso código, considerando a nossa teoria. Podemos então concluir que nosso código ainda não está finalizado. Então, devemos corrigir o código, até erros não sejam mais identificados.


Por fim, gostaria de frisar que o uso do Pex / Code Digger não substituí os testes de unidade "manuais" que costumamos fazer. O que essa abordagem faz é complementar o TDD, nos dando mais uma opção para podermos identificar problemas no nosso código, que é basicamente a função de um teste.

[]'s

ATUALIZAÇÃO: Agora não existe mais a limitação de efetuar a exploração de código apenas em projetos do tipo Portable Class Library. Agora podemos colocar as teorias nos projetos de testes, que é o lugar mais apropriado.
Para isto, basta colocar a opção DisableCodeDiggerPortableClassLibraryRestriction como true, nas opções do Pex no Visual Studio.


Um comentário: