Integração Contínua x One-Step-Build x One-Step Deployment

by alisson.vale 26/3/2006 20:13:45

Em um projeto XP a Integração Contínua deve ser encarada como um importante agente transformador. Ela é o primeiro passo para que o software saia da máquina de um desenvolvedor e apareça na máquina de um cliente para que este o utilize. Na minha visão, a IC afeta diretamente e de forma significativa um projeto XP. Ela pode dizer, por exemplo, o quão rápido será o feedback do seu cliente. Pode dizer também quanto tempo um desenvolvedor terá que gastar para incorporar seu código no sistema e quão confiante o time estará depois de modificações no código, pois este sabe que uma “Successfull Build” é o resultado da passagem do código por vários níveis de checagens. Um integração contínua executa no mínimo três operações básicas: Recupera o código-fonte de seu repositório em sua versão mais atualizada, compila e monta seus diversos componentes e executa todos os testes de unidade sobre esses componentes recém-compilados. Ou seja, nesse cenário, tivemos dois níveis de checagem: compilação e testes de unidade.

De forma geral, uma IC que apenas compila e executa testes de unidade é o suficiente para o seu propósito, que é permitir que os códigos de  vários desenvolvedores estejam sempre sintonizados e funcionando de forma correta. Este processo deve ser simples e rápido o suficiente para ser executado em até 10 minutos. Esta é uma média de tempo que pode ser considerada para a maioria dos projetos. Há projetos em que esse tempo acaba sendo maior. O ideal é que a IC possa acontecer várias vezes por dia e, que os desenvolvedores não precisem ficar parados esperando longos processos de integração.

Ao contrário do que muitos pensam, a IC não precisa ser feita de forma automática. Ela é uma atividade que pode ou não ser feita com o uso de ferramentas. Se, depois de terminar seu trabalho, um par de desenvolvedores sentar em uma máquina, baixar manualmente o código-fonte, compilar esse código e rodar seus testes, tudo de forma manual, ainda assim será um bom processo de IC.

Com o uso de ferramentas de script você pode fazer seu processo de IC se beneficiar do conceito de One-Step Build. Um One-Step Build é um processo que permite a geração de uma build de um sistema por meio de apenas um único passo, que pode ser um click de botão ou a execução de um programa. O próprio script criado se encarrega de rodar todos os passos necessários para a geração da build. Se os passos necessários para execução dessa build passarem por “Obter a última versão do código”+”Compilar”+”Rodar testes de unidade”, este processo de One-Step-Build poderá ser utilizado como apoio para a IC. Há várias ferramentas que podem te ajudar nisso. As mais conhecidas são o Ant, o NAnt, o MSBuild e, mais recentemente, o Ruby Rake (a sensação do momento nesse assunto).

Aqui no nosso projeto, nós usamos com muita satisfação o NAnt e o MSBuild. Ambas as ferramentas são usadas no mesmo processo. O NAnt roda 90% do processo. Deixamos o MSBuild responsável apenas por algumas tasks relacionadas com os processos de compilação e merging da parte web da nossa aplicação, que está em ASP.Net 2.0.

Mas o One-Step Build ainda não é suficiente para reduzir em sua totalidade o que há entre o desenvolvedor e o cliente. Ainda há um vazio entre o resultado da integração contínua e o software efetivamente funcionando no ambiente do usuário.

Depois de integrado, em tese, o software estaria pronto para ser disponibilizado em ambientes diversos para testes ou para uso efetivo de seus usuários. Só em tese mesmo... mas há muito o que fazer ainda. Um software integrado, é um software cujo código-fonte foi validado e checado naquilo que é possível em termos de automação. O compilador checa sintaxe, compatibilidade de tipos e a validade de mensagens enviadas entre objetos. Ainda precisamos checar se os objetos respondem satisfatoriamente às regras e ao propósito para os quais eles foram codificados. Isso é feito com testes de unidade. Mas ainda há um longo caminho a andar para que o software possa estar na máquina de um usuário pronto para ser utilizado e testado. 

Percorrer este caminho, significa ter o que chamamos de One-Step Deployment. Esse é um grande desafio para qualquer equipe de desenvolvimento de software. Pense em todos os passos que você precisa tomar para levar seu software do repositório de código-fonte direto para a máquina do usuário. Atualizar o aplicativo, estruturas de banco de dados, arquivos de configuração, módulos rodando em diferentes máquinas com diferentes tecnologias; considerar restrições de segurança, ambientes distribuídos e, como se já não estivesse complicado, você ainda tem que deixar seu processo informar aos usuários o que foi alterado entre uma versão e outra. Difícil? Sim. É complicado mesmo... Mas o tamanho dos benefícios é proporcional ao tamanho do desafio.  Quando você conseguir executar todos estes passos por meio de um “apertar de botão” você terá alcançado o tão sonhado “One-Step Deployment”.

Em nosso projeto, depois de muito esforço, nós conseguimos chegar a uma solução de Gerência de Configuração em que unimos Integração Contínua com One-Step Build e One-Step Deployment.

Para aqueles que querem conhecer essa solução eu estarei postando semanalmente um overview de todas as etapas que precisamos vencer para chegar a este resultado.

Acompanhe!

Agile = Version N to N+1?

by alisson.vale 18/3/2006 20:55:52
       
Há poucos dias atrás Ralph Johnson (Gang of four, lembram-se?) publicou, em seu blog, um artigo contendo algumas idéias realmente muito interessantes: "Software Development is program transformation". Em suas palavras:

"The purpose of work on existing software is to transform it to the new version. Since almost all work on software is converting version N to version N+1, almost all work on software is program transformation."

Pra mim, essa é uma frase muito arrebatadora. Acho que essa idéia sempre foi óbvia pra mim, mas nunca tinha parado pra pensar que acreditar nessa tese pode fazer você drasticamente entender como funciona um projeto ágil. O artigo do Ralph Johnson me fez chegar a uma definição simplória, mas no mínimo intrigante, sobre o que realmente podemos chamar de “desenvolvimento ágil de software”: A arte de minimizar tempo e esforço para a conversão de um software da versão corrente para uma próxima versão, de forma a se aproximar cada vez mais dos desejos de seus usuários.

Pense bem... todas as práticas do XP e de vários outros métodos convergem de certa forma para essa definição. Vamos examinar as práticas da primeira edição do XP por exemplo:

Jogo do Planejamento, Cliente Presente: Qual é o conjunto mínimo de funcionalidades que eu posso entregar, de modo que o cliente possa avaliar e se beneficiar de forma efetiva e imediata do trabalho realizado com a maior freqüência possível? Se descobrirmos algo no meio do processo, de que forma podemos nos adaptar para que o foco na satisfação do cliente seja mantido? Essas duas práticas nada mais são do que modos eficientes de se chegar a uma versão n+1 tendo como meta agregar o máximo de valor para o software sob o ponto de vista do cliente em um dado momento.
Entregas Freqüentes, Integração Contínua: Quantos mais entregas eu fizer, mais próximo estarei de satisfazer as necessidades de meus usuários. Quanto mais eles começarem a usar o produto, maior será a nossa sintonia. Se o código não for integrado continuamente, o momento da entrega será muito penoso e sujeito a erros. Portanto, uma boa Integração Contínua facilita o processo de entrega freqüente e, portanto, diminui o trabalho de se fazer a versão n virar n+1.
Metáforas, Design Simples, Refactoring e Test Driven: Essas práticas te direcionam a uma aplicação fácil de ser manipulada. Melhor do que isso, te levam a uma aplicação cuja manipulação é muito mais segura. Te levam a um bom design. Mas o que é um bom design? Não é aquele que permite melhorar, estender ou alterar a aplicação de forma mais fácil e segura? Quanto mais fácil é alterar a aplicação, mais fácil será incrementar suas versões e mais ágil será o projeto, pois a capacidade de se entregar funcionalidades é muito aprimorada.
Programação em Pares, Propriedade Coletiva, Padrões de Codificação, Ritmo Sustentável: Essas práticas atuam diretamente sobre a capacidade de produção dos desenvolvedores. Elas os levam a entender melhor o que estão fazendo, aumentando a velocidade e a qualidade com que implementam as funcionalidades da versão n+1.

Em termos gerais, cada ação de uma equipe de desenvolvimento que atue diretamente no sentido de facilitar, agilizar ou assegurar a qualidade da transformação do seu software de uma versão n para uma versão n+1 é um poderoso elemento catalisador para a agilidade de seu projeto. Algumas práticas de Arquitetura e Gerência de Configuração, apesar de não muito formalizadas na semântica dos processos, são extremamente importantes no sentido de complementar o projeto com recursos que ajudam-no consideravelmente no seu dia-a-dia. Na área de Gerência de Configuração eu citaria: Branching, automação de deployment, de montagem de ambiente, de execução e construção de cenários de testes. Também citaria a formalização de arquitetura (incluindo aí uma arquitetura que facilite algumas práticas como o TDD), padrões de codificação e regras para análise estática de código.
       
Compreender o processo de incrementar software, é, no meu entender, a chave para o amadurecimento e para o sucesso daqueles que querem conduzir projetos de software vencedores. Cada vez mais técnicas e ferramentas nos ajudam nessas atividades, cabe a nós saber como balanceá-las de modo a contribuir para a agilidade dos nossos projetos.

Test Driven Revisited

by alisson.vale 15/3/2006 02:54:54

É impressionante como o desenvolvimento orientado a testes transforma o jeito como se escreve código. Também é impressionante como o TDD muda a concepção das coisas. Invertemos nossa forma de trabalhar - agora escrevemos testes antes do código. Nossos compiladores se transformaram de meros "Type checkers" para grandes parceiros nos frequentes processos de red-green-refactor que já estamos tão acostumados a executar. "Faça isso", "faça aquilo"...  "crie esta classe", "codifique este método", "implemente esta interface"... Outro dia, ouvindo um podcast do Kent Beck, descobri que o fato do TDD promover a qualidade do código já não é tão importante. Qualidade é um atributo momentâneo. Sobe e desce a cada release liberada.  Veja que não estou dizendo que qualidade não é importante. Muito pelo contrário, qualidade é essencial e é fato fundamental para a sobrevivência de qualquer produto. Mas qualidade não deve ser tratada como uma meta a ser alcançada a qualquer custo. Ela é uma consequência natural de um outro atributo ainda mais importante: Saúde. Como um organismo vivo, um software é saudável quando ele se defende bem de agressões externas - mudanças de requisito, novas funcionalidades, desafios tecnológicos. Software parado é software morto. Software que evolui com dificuldade é sofware convalescente.  

 "My goal is not quality software, my goal is healthy software" (K.Beck).

Mais do que promover a qualidade do software, TDD promove saúde. Great! Um ponto a mais para quem o utiliza. Mas não é só isso... como nos comerciais da Polishop, levando o TDD você ainda leva totalmente grátis a necessidade e rever todo o design de sua aplicação para que você possa se beneficiar da técnica de forma plena. Em resumo, TDD promove um melhor design. Faça a seguinte experiência: Selecione aleatoriamente uma classe qualquer do seu sistema. Você consegue testá-la em alguns minutos? Não? Então sua classe pode ter problemas de acoplamento e/ou coesão (leia mais em Michael Feathers).

 Nossa conclusão é que um teste escrito dessa forma é muito mais eficiente:

        [Test]
        public void LiquidarDocumentoComCaixaAbertoTeste()
        {

            Id idCaixa = new Id(93);
            Id idBoleto = new Id(120922);
           
Id idUsuario = new Id(2);
            const double VALOR_BOLETO = 392;

            ResultList list = new MemoryResultList();

            CaixaVO caixaVO = FactoryVO.GetCaixaVO(idCaixa, idUsuario, TestEnvironment.Hoje(), TestEnvironment.DataInDefinida());

            BoletoVO boletoVO = FactoryVO.GetBoletoVO(idBoleto, TestEnvironment.Hoje(), Boleto.STATUS_ABERTO, VALOR_BOLETO);

            Tesouraria operadorTesouraria = new Tesouraria();

            operadorTesouraria.LiquidarDocumento(caixaVO, boletoVO, TestEnvironment.Hoje(), VALOR_BOLETO, false, list);

            BoletoVO voBoletoVerificacao = (BoletoVO)list.GetObjectFromListById(typeof(BoletoVO), idBoleto);

            LiquidacaoDocumentoVO liquidacaoVO = (LiquidacaoDocumentoVO)list.GetObjectFromListByIndex(1);

            Assert.AreEqual(Boleto.STATUS_PAGO, voBoletoVerificacao.Status);
            Assert.AreEqual(idBoleto, liquidacaoVO.Boleto);

        }

Este teste é simples, mas ilustra bem como o design da aplicação pode fazer a diferença. A primeira mudança de paradigma é o uso do Dependency Injection. Push, don’t Pull! O objeto que está sendo testado recebe todos os outros objetos que colaboram com ele. Além de permitir a testabilidade da classe, essa técnica ajuda a manter o nível de acoplamento sob controle. Quanto mais objetos uma classe precisar receber, maior será a desconfiança de que algo está errado e de que as responsabilidades precisam ser redistribuídas. O objeto Tesouraria representa uma metáfora de design e sua função é selecionar e orquestrar objetos para a execução de operações co-relacionadas. Como seu estereótipo é de domínio, sua execução não depende de bancos de dados, bibliotecas de terceiros, etc. Ou seja, ela, como todas as outras desse estereótipo, é auto-testável.

E quanto ao acesso ao banco? É difícil, hoje em dia,  imaginar uma aplicação que não dependa de um banco de dados ou de um meio de armazenamento qualquer. Fazer acesso a banco em objetos de domínio não é uma boa opção. Em nossa arquitetura, o acesso a banco é feito apenas para alimentar a camada de domínio, mas não faz parte dela. Uma camada de Serviços recupera informações persistidas de uma camada DAO. Para testar a camada de serviços podemos usar Mock Objects ou utilizar um framework de cenários que criamos para gerar um cenário no banco de dados compatível com o teste que se deseja realizar. Veja o código que testa uma classe da camada de serviço com o apoio de um framework de cenários:

        [TestFixtureSetUp]
        public void CriarCenario()
        {
            ScenarioManager.RunScenario("001");
        }

        [Test]
        public void ObterBoletoTeste()
        {
            string sqlPrimeiroBoletoAberto = "SELECT TOP 1 * FROM " + BoletoVO.TABLE_NAME;
            sqlPrimeiroBoletoAberto += " WHERE " + BoletoVO.COL_STATUS + " = 'A'";

            DataRow dadosBoleto = ScenarioManager.GetInfoInTheFirstRow(sqlPrimeiroBoletoAberto);

            Assert.AreEqual(false, dadosBoleto == null, "Deveria existir um boleto no Banco de Dados neste ponto.");

           int idBoleto = Convert.ToInt32(dadosBoleto[BoletoVO.PK_NAME]);

           ServicosBoleto service = new ServicosBoleto();
           BoletoVO boletoVO = service.ObterBoleto(idBoleto);

           Assert.AreEqual(boletoVO.Id, new Id(idBoleto));
          
Assert.AreEqual(boletoVO.DataVencimento, Convert.ToDateTime(dadosBoleto[BoletoVO.COL_DATA_VENCIMENTO]));
           Assert.AreEqual(boletoVO.Status, Convert.ToString(dadosBoleto[BoletoVO.COL_STATUS]));
           Assert.AreEqual(boletoVO.ValorDocumento, Convert.ToDouble(dadosBoleto[BoletoVO.COL_VALOR_DOCUMENTO]));
       
}

        [TestFixtureTearDown]
        public void TearDown()
        {
            ScenarioManager.CleanUp();
        }

O Framework de cenários é configurado em um arquivo Xml, algo como o mostrado a seguir:

<?xml version="1.0" encoding="utf-8" ?>

<Cenarios RootScriptFolder=" \Phidelis.Src\Kernel\Financeiro\ TestesFuncionais\Cenarios\Scripts">

    <Script Id="CleanUp_01" Type="CleanUp" Description="Exclusão de todos os registros" File="CleanUp.sql" />
   
<
Script Id="001" Type="Cenario" Description="Ficha Financeira com um boleto de rematrícula aberto" />

</
Cenarios>

Este tipo de teste é categorizado como um “Teste Funcional” e ainda é um teste de desenvolvedor e não um teste de Quality Assurance. No entanto, só é executado sobre a camada de serviços, que é a camada consumida pela Interface de Usuário.

Depois de 2 anos trabalhando com TDD e mais de 1000 testes sendo executados a cada build, chegamos a importantes conclusões com relação ao nosso processo de escrever testes. Apenas com uma mudança arquitetural, poderíamos ter um aumento fenomenal na quantidade e na qualidade dos testes unitários, além de podermos executá-los de forma mais rápida e com mais frequência. As técnicas de Design realmente fazem a diferença, e são os testes unitários que nos têm levado a descobri-las e nos motivado a incorporá-las em nossos sistemas.


Tags:

Coding

Sobre o Autor

Alisson Vale Alisson Vale
Líder de Projeto da Phidelis Tecnologia.


E-mail me Send mail
      Sign in

Últimos posts

Últimos comentários

Termo de Responsabilidade

Este site apresenta apenas opiniões pessoais. Não necessariamente representa as opiniões ou práticas da Phidelis Tecnologia.

© 2008