February 4th, 2009 §
No último artigo, falei um pouco sobre a preocupação arquitetural que deve permear os testes e fiquei devendo um exemplo.
Para não entrar em detalhes específicos do Rails e viciar a discussão, me lembrei de um exemplo que li há vários anos, quando estava começando a estudar XP, de um sessão entre dois programadores experientes fazendo pair-programming para chegar em um corpo de testes expressivo com base em um problema bem específico.
O exemplo é real e foi protagonizado por Robert C. Martin, um dos assinantes originais do Agile Manifesto e Robert S. Koss. Para ler o exemplo, siga para a página do mesmo.
Como é possível ver claramente pelo exemplo, a implementação secundária–representada pela classe Scorer–não possui um teste em particular. Entretanto, está fielmente testada através do seu relacionamento com o objeto mais importante. A preocupação arquitetural está no que as duas classes fazem em conjunto e não com o que cada uma delas possui como função. Note, inclusive, que classes secundárias são consideradas e descartadas sem que as mesmas sejam objeto de testes mais do que de passagem.
Obviamente, em uma arquitetura saudável, classes dominantes aparecerão e terão um foco maior em testes. O importante, então, é manter a visibilidade do domínio que está sendo testado.
No caso específico do Rails, o grande problema está em como aplicações são geralmente desenvolvidas. Por causa da separação convencionada do domínio MVC da aplicação, muitos desenvolvedores acreditam que essa é a única forma que se pode desenvolver aplicações usando o framework. O alvo, quando usando o Rails, não deve ser aplicar essa estrutura cegamente mas conseguir segregar papéis e funções de modo que o todo seja coerente.
Essa é uma visão bem geral do assunto e poderíamos nos estender em vários aspectos. Perguntas e comentários são bem-vindos.
February 3rd, 2009 §
No meu último artigo, comentei bastante sobre a minha opinião de que testes devem ser sobre o relacionamento entre partes específicas do seu código e não sobre interfaces ou contratos. Na minha experiência, os testes mais duradouros e de maior valor são aqueles que exercem as interfaces e contratos indiretamente, através do arquitetura particular oferecida pelos mesmos.
O ressurgimento de testes como uma ferramenta ágil é uma coisa recente e, obviamente, uma excelente oportunidade para conversar sobre técnicas, filosofia e metodologia de desenvolvimento. Em especial, a comunidade Rails tem feito um trabalho excepcional de evangelização sobre o assunto, tornando testes um participante de primeira classe no discurso de desenvolvimento de software.
Entretanto, como é fácil acontecer, o próprio sucesso do assunto está se convertendo em uma fonte de perigos para desenvolvedores iniciantes ou que não tenham tanto familiaridade com TDD e BDD. A própria multiplicação dos pães, digo, dos frameworks de teste está contribuindo para isso no sentido de que, em um afã de criar mais features do que o concorrente, alguns framework estão simplesmente promovendo técnicas péssimas de testes em troca de um falso senso de segurança.
Isso volta um pouco na discussão sobre a diferença entre TDD e BDD, mas acho que o ponto merece uma ênfase. Em resumo, é imprescindível evitar substituir arquitetura, mesmo quando se está fazendo TDD, por meros testes.
Isso fica mais fácil de ser percebido com algumas ilustrações. Tomando o Shoulda como exemplo, é muito comum ver código como o seguinte:
class UserTest < ActiveRecord::TestCase
should_belong_to :account
should_have_many :posts
should_have_named_scope('recent(5)').finding(:limit => 5)
should_have_index :age
end
Esse tipo de código não prova absolutamente nada sobre o desenho próprio de sua classe. O código acima:
É redundante, porque as três primeiras cláusulas já serão testadas automaticamente em outras partes do código, especificamente em controller;
É quebradiço, porque é diretamente relacionado à implementação e não ao comportamento da classe em si;
É pouco mais do que um teste de sanidade para descobrir se o desenvolvedor colocou algumas poucas linhas de código em seu modelo;
Expõe detalhes de implementação, como no caso do matcher para índice.
Em outras palavras, todos os testes acima são absolutamente inúteis. O teste de escopo é o único com algum valor para o teste do modelo em si, mas continua sendo redundante.
Pior ainda, existem exemplos como shouldhavebeforesavecallback, proveniente do Remarkable. Esse é o tipo de asserção que chega a ser contraproducente. É um teste que expõe a funcionalidade subjacente de um modelo, que por regras de encapsulamento deveria ser completamente isolada e invisível para as demais partes da aplicação, é um desvio completo do que TDD representa.
Testes, mais uma vez, são sobre interoperabilidade entre facetas do código. São parte de uma conversa arquitetural que procura se focar o mínimo em detalhes internos de implementação. O objetivo é escrever o menor corpo possível de testes–axiomas–que possa dar uma indicação da validade de um dado corpo de código. E como eu repito freqüentemente aqui, simplicidade é um alvo explícito de boas arquiteturas.
January 31st, 2009 §
Eu gosto bastante do que Joel Spolsky e Jeff Atwood escrevem, mas a última conversa entre os dois no postcast regular que eles mantém realmente releva uma boa falta de conhecimento sobre o que testes e TDD realmente representam.
No núcleo do argumento dos dois está a idéia de que alta cobertura por meio de testes–Jeff Atwood menciona a falta de 95% ou mais–torna a manutenção dos próprios testes problemática considerando a proporção de testes que precisa ser mudada quando o código em si também é modificado. Um argumento secundário é que testes são mais indicados para código legado do que código que está sendo desenvolvido, a menos que o mesmo tenha rigidez natural como a especificação de um compilador.
A solução para o segundo argumento é simples: todo código é legado. Simples assim. Código que entrou em produção é legado por definição e o argumento de que há uma diferença entre código com “mais idade” e “menos idade” é dúbio no pior dos casos.
Lendo o diálogo entre os dois, é possível perceber que há uma confusão sobre o que testes realmente representam–especialmente quando os dois falam sobre a relação entre testes e arquitetura, algo que em um contexto ágil é normalmente referenciado por TDD e BDD.
Essa confusão–de que testes são feitos para cobrir a interface de uma classe ou método–é extremamente comum mesmo entre os próprios proponentes de testes, seja entre aqueles que propõem testes como ferramenta de design como é o caso de TDD e BDD, seja entre os que simplesmente os usam para validar o funcionamento posterior do código.
Eu posso concordar parcialmente com o argumento de que 100% de cobertura geralmente é desnecessária. De fato, 100% de cobertura nunca representa uma garantia absoluta de que seu código, e por extensão sua arquitetura, não possuem problemas.
Primeiro porque 100% de cobertura real é algo virtualmente impossível de ser conseguido para um corpo mais complexo de código (e realmente impossível se o mesmo tem dependências externas). Segundo, por que não importa a quantidade de testes que você tiver, a complexidade ciclomática sempre vai acabar com seu dia nas horas mais inapropriadas. Não importa o quantidade de white-box ou black-box testing que você possuir, cobertura é alvo exponencialmente proporcional à evolução do seu código.
Existe também um outro fator representado por uma variação casual da regra 80/20. Os maiores ganhos de testes estão nas partes mais complexas do seu código, é claro, mas os ganhos reais geralmente aparecem nos desvios pequenos que pegam você de surpresa. Nesse ponto, quando maior a cobertura, maior facilidade de introduzir novos testes.
E aqui está o verdadeiro ponto em que o argumento de Spolsky e Attwood falha: testes não são sobre interfaces, APIs ou contratos. Antes, eles são sobre o relacionamento entre as partes distintas de seu código. Nessa diferença essencial está um dos grandes debates recentes da comunidade ágil: qual é a diferença real entre TDD e BDD?
A minha resposta está centrada em uma pequena reinterpretação de TDD. Ao invés de usar o termo como Test-Driven Development, eu prefiro ver a questão toda como Test-Driven Design.
Se você está usando testes como uma ferramenta para guiar o design de sua aplicação, isso significa que você estará preocupado mais em saber com as peças se encaixam do que como elas funcionam individualmente, como mencionado acima.
Joel diz:
But the real problem with unit tests as I’ve discovered is that the type of changes that you tend to make as code evolves tend to break a constant percentage of your unit tests. Sometimes you will make a change to your code that, somehow, breaks 10% of your unit tests.
Você realmente pode fazer uma mudança que quebra 10% de seus testes, mas para que isso aconteça, o design do sistema precisa estar tão falho e seus testes tão quebradiços que os mesmos podem ser jogados fora sem remorso.
Há pouco mais do que duas semanas, eu fiz uma modificação considerável em uma biblioteca que escrevi. Em linhas gerais, significa trocar o motor de um middleware de DRb (Ruby distribuído) para JSON over HTTP. Esse código em particular está 100% coberto.
Por causa das diferenças entre os protocolos, uma quantidade de código significativa teve que ser modificada. Mas apenas três ou quatro novos testes foram escritos e nenhum dos outros foi modificado. Código foi movido, reorganizado em classes diferentes e mesmo assim, os testes resistiram bravamente.
A explicação é simples: enquanto existem testes que realmente testam como funções específicas funcionam, a vasta maioria destes testes é focado em como as várias partes da aplicação trocam informações: se os dados que saíram daqui nesse formato chegaram do outro lado do formato necessário, se essa AST foi reorganizada apropriadamente de modo que o gerador do outro lado da equação possa produzir a linguagem apropriada e assim por diante.
Jeff continua com outra asserção:
Yeah, it’s a balancing act. And I don’t want to come out and say I’m against [unit] testing, because I’m really not. Anything that improves quality is good. But there’s multiple axes you’re working on here; quality is just one axis. And I find, sadly, to be completely honest with everybody listening, quality really doesn’t matter that much, in the big scheme of things…
Isso é algo que me fez repensar todo o contexto da discussão. Eu realmente fico espantado de ver alguém que considera Peopleware e The Mythical-Man Month como referências básicas dizer isso. Ambos os livros focam em arquitetura dirigida por qualidade como um foco para produzir código mais robusto, entregue em menor tempo e que traz mais valor para o negócio e para o usuário. Os parágrafos finais da transcrição são realmente decepcionante nesse aspecto.
Para finalizar, TDD realmente não é um fim em si próprio. Mas o argumento de que usar testes é um desperdício de tempo–seja para quem está cobrindo 100% do seu código de forma viável ou para quem usa testes como validação de funcionalidade–é simplesmente falho.
Joel é conhecido bastante por seu pragmatismo em relação à correção de bugs. Testes são uma maneira bastante pragmática de garantir que aquele conjunto particular de situações de falha não vá se repetir por engano em alguma codificação posterior. Isso é business value, algo que tanto Joel como Jeff valorizam.
No final das contas, pragmatismo é o que realmente conta. E testes, quanto feitos da maneira certa, são algumas das mais pragmáticas ferramentas que um programador tem à sua disposição.
January 18th, 2009 §
O Luiz Rocha escreve com razão sobre os perigos de confundir Agile com um conjunto de métodos que devem ser seguidos para que o “processo” funcione. É uma coisa natural de organizações–que por extensão são pessoas–procurarem um ponto do balanço entre necessidades discordantes e este é um dos pontos mais propícios para transformar a filosofia Agile em uma metodologia que pouco difere do tradicional Waterfall.
Por outro lado, existe uma tentação muito grande de pensar em Agile com o oposto exato do Waterfall e transformar isso em uma desculpar para não ter nenhum processo ou método coerente de desenvolvimento. É muito comum encontrar empresas imersas na desorganização que descrevem os seus processos como ágeis.
“Não escrevemos documentação, oras. Somos Agile. Preferimos software que funciona a documentação compreensiva. Não é isso o que o manifesto diz?”
É claro que o manifesto termina com a afirmação: That is, while there is value in the items on the right, we value the items on the left more. Isso não impede que tudo seja abandonado em favor de uma pretensa agilidade.
Como o Luiz diz, não é um task board que faz uma empresa ou equipe ágil. Tampouco é a falta de planos em contraponto a uma resposta completamente desorganizada a mudanças.
Agile exige um compromisso com o detalhe:
Para valorizar interação e não processos é necessário entender que o nível de detalhe requerido do primeiro é maior que o do segundo.
Para valorizar software que funciona é preciso uma cultura de qualidade, o que implica em muito mais trabalho do que escrever documentação compreensiva–e isso vai muito além de colocar no quadro de tarefas itens para escrever testes, algo que nada mais é do que transformar o Agile em Waterfall.
Para valorizar a colaboração do cliente é preciso ouvir e raciocinar sobre o que foi ouvido, transformando isso em valor. Negociar contratos é muito mais simples.
Finalmente, para valorizar resposta rápida a mudanças é necessário uma atitude de constante atenção ao que está sendo feito e como isso vai evoluir no futuro. Novamente, seguir um plano é incomparavelmente mais fácil, mesmo que o mesmo seja completamente falho.
Agile não é, e nunca foi, uma opção para aqueles que querem tomar a rota mais fácil. Excelência técnica, cultura de qualidade, atenção ao detalhe–transformar isso em segunda natureza requer anos de prática: o mesmo tipo de esforço que um artesão passa a vida aplicando.
Se você quer algo simples, esqueça Agile. Não é para quem não tem um compromisso com o tempo necessário para que coisas permanentes floresçam.
December 16th, 2008 §
Eu sempre acreditei que a convergência tecnológica não se daria na Web e sim no celular. Quanto mais o tempo passa, mais estou certo disso. Quanto mais cedo as empresas se convencerem disso–e pode ter certeza de que a Apple está olhando dez anos à frente nisso, ergo o iPhone–mais elas estarão próximas de seus usuários.
Das minhas últimas leituras sobre redes sociais, um assunto que meio que passa desapercebido da literatura é a convergência que está sendo realizada em torno de gaming spaces.
No Brasil–e mesmo nos Estados Unidos e Europa–a falta de jogos de realidade consensual mais ativos como uma atividade regular mascara isso mas basta olhar para a Ásia e ver que mais uma vez estamos atrasados no assunto. No Japão, jogos envolvendo geo-localização e realidade aumentada são um dos setores mais ativos da indústria móvel. Na China, a moeda virtual do QQ, a maior plataforma móvel local, é tão forte que não só é aceita em lojas comuns como incomodou o governo ao ponto de regulamentação ser necessárias.
Declarar que o ser humano é social é óbvio. Perceber a interseção disso com o espaço de jogos em que existimos não é tão fácil. Nossas carreiras são literalmente arquiteturadas ao redor de jogos que possuem implicações reais e imediatas. Transpor isso para um World of Warcraft é só uma maneira secundária de visualizar a questão–igualmente válida e igualmente importante.
É claro, existe uma grande dependência em torno de grandes transformações necessárias para uma materialização de alguns cenários. Primeiro, sem feedback háptico é mais difícil transpor a barreira para a parte aumentada da realidade–e um tanto ou quanto menos satisfatório. Segundo, um ambiente seguro, criptografado e distribuído por padrão é necessário. Terceiro, alguns jardins fechados precisam ser abertos pelos menos parcialmente.
Os dois primeiros pontos acima podem ser resolvidos com mais alguns anos de avanço tecnológico. O último vai depender de um tipo de reengenharia humana em escala global que pode demorar mais tempo para acontecer. Já começou mas pode tomar décadas ainda no pior caso.
De qualquer forma, o ponto inicial permanece: convergência móvel é um fato agora e vão sair na frente as empresas que aproveitarem isso.
December 12th, 2008 §
Meu irmão, como já citei outras vezes aqui, tem uma pequena operação em Belo Horizonte prestando serviços em software livre para empresas no Brasil e nos Estados Unidos. Atualmente, a maior parte do trabalho é em Rails e estamos sempre conversando sobre como melhorar e aumentar a eficiência do que ele e o pessoal que trabalha lá estão fazendo.
Recentemente ele me visitou na WebCo e ficou bem curioso ao ver as dezenas de post-its colados nas várias janelas representando os diversos projetos da empresa. Expliquei um pouco de Agile–usávamos várias técnicas agéis quando eu ainda estava na empresa–mas Scrum era uma coisa que eu ainda não tinha comentado com ele.
Depois que ele voltou para Belo Horizonte, ele me ligou pedindo que eu explicasse um pouco mais sobre os tais dos sprints. Explique mais um pouco e sugeri que ele experimentasse mesmo com a equipe pequena.
Mais algum tempo passou e em mais uma de nossas conversas ele me explicou com tinha adaptado o que eu estava falando para a realidade de sua empresa:
Com R$126 ele comprou um quadro, alfinetes, post-its coloridos, canetas e marcou as tarefas de vários projetos com cores diferentes para identificar os mesmos. Mesmo tarefas administrativas da empresa foram colorizadas e colocadas no quadro. Ele também separou o progresso das mesmas em “a fazer”, “em progressos” e “feitas”, como no Scrum, mas sem a formalização de daily meetings.
Como isso, ele começou a poupar uma quantidade enorme de trabalho de gerenciamento e acompanhamento de tarefas ao mesmo tempo em que dava maior visibilidade a todos da empresa sobre o que estava acontecendo. Eventualmente, o quadro também se tornou uma reflexão do sistema Trac que ele usa para controle de tickets aumentando ainda mais a produtividade da equipe.
Para alguns puristas, a adaptação que ele fez seria considerada tão longe do Scrum quando um processo waterfall. Esse é o ponto que a maioria das pessoas perde sobre Agile:
Agilidade não tem absolutamente nada a ver com formalização. Assim como waterfall funciona para determinados tipos de processo–existem milhares de casos de extremo sucesso usando a filosofia–agilidade só funciona quando a idéia em si é comprada e adaptada à realidade específica dos processos necessários.
No caso do meu irmão, daily meetings com formalizados no Scrum não tem o menor sentido com a equipe pequena e geograficamente distribuída. Aplicar todos os parâmetros que o Scrum oferece teria o efeito contrário de reduzir sua produtividade.
Antes de tudo vem o lúdico, a diversão. E isso leva tão facilmente ao lucro que é impressionante pensar no número de pessoas enganadas quanto à história toda. Mas eu me divirto e sempre sou lembrado de pensar diferente quando vejo adaptações como a feita pelo meio irmão que funciona e resulta em efeitos permanentes positivos. Melhor do que isso é difícil.
November 13th, 2008 §
Flog é outro analisador de complexidade para Ruby. Diferentemente do Saikuro, que mede a complexidade ciclomática real do código, o Flog tem o propósito de mostrar padrões de tortuosidade no seu código. É mais uma ferramenta interessante e de fácil uso para analisar a evolução da complexidade do seu código:
Instalação
Para instalar o Flog, basta rodar o comando gem:
~$ sudo gem install flog
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um programa de exemplo
Para demonstrar o uso da ferramenta, vamos utilizar o mesmo exemplo que mostrarmos no artigo sobre o uso do Saikuro.
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
if @options[:fields]
@options[:fields].each do |field|
hash[field] = @object.send(field)
end
end
if @options[:extrafields]
@options[:extrafields].each do |field|
hash[field] = @object.send(field.tos + "field")
end
end
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLEFIELD = "Test"
AUTHORFIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLEFIELD, AUTHORFIELD)
class << @object
def extra_field
"Extra"
end
end
end
def testhashid
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def testhashfields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assertequal TITLEFIELD, result[:title]
assertequal AUTHORFIELD, result[:author]
end
def testhashextrafields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extrafields => [:extra])
assertequal EXTRAFIELD, result[:extra]
end
end
Salve esse arquivo para um arquivo chamado hasher.rb e rode-o com o comando abaixo para verificar sua execução:
~$ ruby hasher.rb
Utilização
Com esse programa, podemos rodar o Flog. O comando é simples:
~$ flog hasher.rb
Resultado inicial
O resultado da execução acima pode ser visto na listagem abaixo:
Total Flog = 57.7 (7.2 +/- 62.8 flog / method)
DocumentHasher#hash: (27.3)
9.0: send
8.6: assignment
5.8: []
5.4: branch
2.8: each
1.9: to_s
1.7: +
1.5: new
1.3: hash
1.3: id
DocumentHasherTest#setup: (8.3)
6.5: sclass
1.3: assignment
1.3: new
0.4: lit_fixnum
Note que o método hash possui um fator de "tortuosidade" de 27 e que os maiores culpados são o envio de mensagens (send) e atribuições. A "tortuosidade" total do programa está em 57.7, que representa o cômputo de todos os métodos do programa.
Alterando o programa
Para fins de exemplo, podemos tentar reduzir a complexidade do método hash como fizemos no artigo anterior usando o exemplo abaixo:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
build_fields(hash, @options[:fields] || {})
build_extra_fields(hash, @options[:extra_fields] || {})
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
protected
def build_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field)
end
end
def build_extra_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field.to_s + "_field")
end
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLE_FIELD = "Test"
AUTHOR_FIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLE_FIELD, AUTHOR_FIELD)
class << @object
def extra_field
"Extra"
end
end
end
def test_hash_id
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def test_hash_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assert_equal TITLE_FIELD, result[:title]
assert_equal AUTHOR_FIELD, result[:author]
end
def test_hash_extra_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extra_fields => [:extra])
assert_equal EXTRA_FIELD, result[:extra]
end
end
Rodando o comando novamente, teríamos:
Total Flog = 58.1 (5.3 +/- 10.3 flog / method)
DocumentHasher#hash: (10.7)
3.2: []
3.0: branch
2.6: assignment
1.5: new
1.3: build_extra_fields
1.3: hash
1.3: build_fields
1.3: id
DocumentHasher#build_extra_fields: (9.4)
4.2: send
2.8: assignment
1.8: to_s
1.6: +
1.3: branch
1.3: each
DocumentHasherTest#setup: (8.3)
6.5: sclass
1.3: assignment
1.3: new
0.4: lit_fixnum
DocumentHasherTest#test_hash_fields: (7.0)
3.0: []
2.6: assert_equal
1.3: hash
1.3: assignment
Note que a complexidade dos métodos caiu substancialmente. Os métodos estão mais simples e mais balanceados. A "tortuosidade" geral subiu um pouco com a adição de algumas chamadas, mas, analisando os métodos individualmente é possível observar que a somatória de complexidade dos três métodos envolvidos é menor do que a complexidade original. Mais uma vez, o código está mais legível e mais testável.
Conclusão
O Flog representa uma métrica arbitrária de complexidade que, mesmo assim, pode ser usada para acompanhar o desenvolvimento de um base de código. Um exemplo disso pode ser visto no texto do Carlos Villela mostrando a evolução do código do Rails ao longo dos anos.
Como de usual, sugestões, críticas e dúvidas são bem-vindos. No próximo artigo, esfolando o seu código Ruby com mais ferramentas de análise de complexidade.
November 13th, 2008 §
Flay é uma ferramenta útil para cercear repetições de código em programas Ruby. Analisando o código semanticamente, o Flay é capaz de pegar repetições em casos que geralmente passam despercebidos no melhor dos códigos.
Em algumas casos, a repetição será inevitável e mesmo necessária para deixar o código mais legível. Geralmente, entretanto, repetição significa código que ainda não está DRY o suficiente.
Instalação
Para instalar o Flay, basta rodar o comando gem:
~$ sudo gem install flay
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um exemplo
Para demonstrar o uso da ferramenta, vamos rodar o Flay contra o código mais recente do Active Record:
O resultado (um trecho apenas), depois de alguns minutos de trabalho com a CPU a 100%, é o seguinte:
Matches found in :defn (mass = 290)
./test/cases/associations/join_model_test.rb:237
./test/cases/associations/join_model_test.rb:262
./test/cases/associations/join_model_test.rb:271
./test/cases/associations/join_model_test.rb:623
./test/cases/associations/join_model_test.rb:650
Matches found in :defn (mass = 270)
./test/cases/validations_test.rb:929
./test/cases/validations_test.rb:937
./test/cases/validations_test.rb:945
./test/cases/validations_test.rb:953
./test/cases/validations_test.rb:961
./test/cases/validations_test.rb:969
Matches found in :defn (mass = 236)
./test/cases/validations_i18n_test.rb:491
./test/cases/validations_i18n_test.rb:510
./test/cases/validations_i18n_test.rb:529
./test/cases/validations_i18n_test.rb:549
Matches found in :scope (mass = 204)
./test/migrations/duplicate/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_1/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_2/3_innocent_jointable.rb:12
./test/migrations/interleaved/pass_3/3_innocent_jointable.rb:12
./test/migrations/missing/4_innocent_jointable.rb:12
./test/migrations/valid/3_innocent_jointable.rb:12
Matches found in :call (mass = 172)
./test/models/author.rb:64
./test/models/project.rb:17
Matches found in :defn (mass = 171)
./test/cases/validations_test.rb:977
./test/cases/validations_test.rb:991
./test/cases/validations_test.rb:1079
Note especialmente código que está sendo repetido em arquivos diferentes. No caso acima, como o código está em arquivos de teste, é provável que a repetição seja desejável para não poluir o escopo dos mesmos.
Vamos tomar um outra caso na biblioteca em si:
Matches found in :if (mass = 165)
./lib/active_record/associations/has_and_belongs_to_many_association.rb:82
./lib/active_record/associations/has_many_association.rb:100
./lib/active_record/associations/has_many_through_association.rb:194
O código repetido em questão é o seguinte:
if @reflection.options[:counter_sql]
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
elsif @reflection.options[:finder_sql]
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
else
@counter_sql = @finder_sql
end
Vendo os arquivos, é possível observar que o código é o mesmo. Considerando que os três arquivos representam classes com um pai em comum seria possível reduzir a repetição movendo o código para um método a classe pai.
Esse tipo de repetição tende a sumir com refatoramentos e o código acima provavelmente sumirá por um processo similar (alguém se habilita para um patch?).
Conclusão
Como é possível perceber, o Flay representa mais uma forma de observar o seu código em busca de coisas que podem ser melhoradas. É claro que isso não deve ser converter em uma obsessão de remover toda e qualquer repetição, o que demoraria mais tempo do que codificar coisas novas.
Como de usual, sugestões, críticas e dúvidas são bem-vindos.
November 12th, 2008 §
Saikuro é uma analisador de complexidade ciclomática para Ruby. A instalação e uso do mesmo são bem simples e serão descritos aqui brevemente.
Instalação
Para instalar o Saikuro, basta rodar o comando gem:
~$ sudo gem install Saikuro
Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.
Um programa de exemplo
Para demonstrar o uso da ferramenta, vamos utilizar um programa de referência. Esse programa será um simples transformador de objetos Ruby em hashes equivalentes e está listado abaixo, com os testes necessários:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
if @options[:fields]
@options[:fields].each do |field|
hash[field] = @object.send(field)
end
end
if @options[:extrafields]
@options[:extrafields].each do |field|
hash[field] = @object.send(field.tos + "field")
end
end
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLEFIELD = "Test"
AUTHORFIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLEFIELD, AUTHORFIELD)
class << @object
def extra_field
"Extra"
end
end
end
def testhashid
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def testhashfields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assertequal TITLEFIELD, result[:title]
assertequal AUTHORFIELD, result[:author]
end
def testhashextrafields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extrafields => [:extra])
assertequal EXTRAFIELD, result[:extra]
end
end
Salve esse arquivo para um arquivo chamado hasher.rb e rode-o com o comando abaixo para verificar sua execução:
~$ ruby hasher.rb
Utilização
Com esse programa, podemos rodar o Saikuro. O comando é simples:
~$ saikuro -c -t -y 0 -w 11 -e 16 -i hasher.rb -o report
Esse comando roda o analisador de complexidade com os seguintes parâmetros:
- -t, para analisar tokens também
- -y 0, para exibir a complexidade de todos os métodos
- -w 11, para exibir warnings para métodos de complexidade superior a 11
- -e 16, para exibir erros para métodos de complexidade superior a 16
- -i hasher.rb, o arquivo a ser analisado (pode ser um diretório também)
- -o report, o diretório onde gerar o relatório de saída
Resultado inicial
O resultado da execução acima pode ser visto, parcialmente, na imagem abaixo, que mostra os resultados somente para a classe em que estamos interessados:

Note que o método hash possui complexidade 5 que reflete os 4 comandos decisórios (2 if e 2 each) mais um ponto de saída pelo cálculo mais simples da complexidade. Os demais métodos, puramente lineares, possuem complexidade 1.
No diretório de resultado há também um arquivo que mostra a quantidade de tokens gerados no programa. Este relatório, com os níveis de aviso baixo, tende a reportar erros mesmo em condições consideradas normais.
Alterando o programa
Para fins de exemplo, podemos tentar reduzir a complexidade do método hash. Digamos que façamos a seguinte alteração:
require "test/unit"
class DocumentHasher
def initialize(object, options = {})
@object = object
@options = options
end
def hash
hash = { :id => @object.id }
build_fields(hash, @options[:fields] || {})
build_extra_fields(hash, @options[:extra_fields] || {})
hash
end
def self.hash(object, options = {})
self.new(object, options).hash
end
protected
def build_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field)
end
end
def build_extra_fields(hash, fields)
fields.each do |field|
hash[field] = @object.send(field.to_s + "_field")
end
end
end
class DocumentHasherTest < Test::Unit::TestCase
Struct.new("HashExample", :id, :title, :author)
TITLE_FIELD = "Test"
AUTHOR_FIELD = "Author"
EXTRA_FIELD = "Extra"
def setup
@object = Struct::HashExample.new(1, TITLE_FIELD, AUTHOR_FIELD)
class << @object
def extra_field
"Extra"
end
end
end
def test_hash_id
result = DocumentHasher.hash(@object)
assert_equal 1, result[:id]
end
def test_hash_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author])
assert_equal TITLE_FIELD, result[:title]
assert_equal AUTHOR_FIELD, result[:author]
end
def test_hash_extra_fields
result = DocumentHasher.hash(@object,
:fields => [:title, :author],
:extra_fields => [:extra])
assert_equal EXTRA_FIELD, result[:extra]
end
end
Rodando o comando novamente, teríamos:

Como os testes demonstram, o programa funciona da mesma forma. E como o relatório de complexidade também demonstra, a complexidade do programa permanece a mesma no geral e a dos métodos diminuiu. O resultado é um programa mais legível e mais facilmente testável.
Conclusão
Embora métricas em si não tenham nenhum poder de tornar o código de um programa melhor, acompanhar a evolução das mesmas é uma ferramenta fundamental para garantir a qualidade do código. Para quem usa o Ruby, o Saikuro é uma ferramenta simples e rápida para isso. Para quem não usa, é fácil achar ferramentas que se adequam a outras linguagens.
Como de usual, sugestões, críticas e dúvidas são bem-vindos. No próximo artigo, ainda hoje, esfolando o seu código Ruby com mais ferramentas de análise de complexidade.
November 12th, 2008 §
Este é o quarto artigo em uma série sobre conceitos de programação. Os demais artigos podem ser encontrados na página de resumo sobre a série. Como nos demais artigos, um aviso: o tratamento dado aos assuntos aqui é informativo e podem existir erros nos mesmos. Como nos demais artigos neste blog, comentários e correções são sempre bem-vindos.
Complexidade Ciclomática
Neste quarto artigo, vamos tratar um pouco de complexidade ciclomática. Apesar do nome esdrúxulo, complexidade ciclomática (também conhecida como complexidade condicional) é uma métrica simples para determinar, como o próprio nome sugere, a complexidade de um programa estruturado (cíclico).
Essa métrica foi desenvolvida em 1976 por Thomas J. McCabe e reflete diretamente o número de caminhos independentes que um programa pode tomar durante a sua execução.
Qualquer desenvolvedor que já tenha testado código em sua vida, sabe que a quantidade de testes necessária para exercitar um determinado trecho de código é diretamente proporcional à sua árvore decisória. Em outras palavras, quanto mais caminhos o código puder tomar (seja por meios de condicionais ou loops), maior a quantidade de testes necessários. E como veremos abaixo, há realmente uma relação direta entre a complexidade ciclomática e a cobertura de um código.
Calculando a complexidade ciclomática
Antes de mostrar exatamente como o cálculo pode ser feito, vamos observar algumas coisas em relação a um programa qualquer. Digamos que você esteja desenvolvendo um programa que lhe dê o maior divisor comum entre dois números. Uma fórmula simples é o algoritmo de Euclides que pode ser descrito da seguinte forma:
Dados dois números naturais a e b, verifique se b é zero. Se sim, a é o maior divisor comum entre os mesmos; caso contrário, repita o processo usando b e o resto da divisão de a por b.
Esse algoritmo pode ser expresso pelo seguinte programa em Ruby (note que ele não está em Ruby idiomático):
require "test/unit"
def euclid(m, n)
if n > m
r = m
m = n
n = r
end
r = m % n
while r != 0
m = n
n = r
r = m % n
end
n
end
class EuclidTest < Test::Unit::TestCase
SETS = [[5, 10, 5], [2, 6, 2], [11, 7, 1],
[80, 64, 16], [2, 2, 2]]
def test_euclid
SETS.each do |set|
assert_equal set[2], euclid(set[0], set[1])
end
end
end
Se o programa acima for executado, ele rodará o caso de teste logo abaixo da função que verificará se a mesma está correta. Você pode adicionar mais casos ao conjunto SETS se desejar.
A função euclid pode ser descrita por um grafo simples que conecta os caminhos entre as várias declarações que a mesma contém. Esse grafo é o mostrado abaixo (clique para expandir):

Com base nesse grafo, podemos definir a complexidade ciclomática de um programa da seguinte forma:
CC = A - N + 2C
Nessa fórmula:
- CC é a complexidade ciclomática
- A é o número de arestas do grafo
- N é o número de nós do grafo
- C é o número de componentes conectados
Como se trata de uma função simples com um único ponto de entrada e saída, o número de componentes é 1 e a fórmula pode ser reduzida para:
CC = A - N + 2
Se a função possuísse múltiplos pontos de saída, entretanto, a complexidade ciclomática seria definida como:
CC = A - N + C + R
Nessa fórmula, R é o número de declarações de saída (em Ruby, o número de returns).
Voltando ao grafo mostra na figura, vemos que o mesmo possui 11 nós e 12 arestas, o que nós dá uma complexidade ciclomática de 12 - 11 + 2, ou seja, 3.
Uma outra maneira bem simples de descobrir a complexidade ciclomática é contar o número de loops fechados no grafo (que são formados por condicionais e loops) e somar ao número de pontos de saída. No grafo acima, temos 2 loops fechados (os if e while) e um ponto de saída, resultando no mesmo valor 3 para a complexidade da função.
Uma coisa interessante é que a complexidade permanece a mesma quando a sintaxe de uma linguagem é levada em questão sem alterar a semântica do programa. Tome por exemplo a versão idiomática do algoritmo em Ruby:
def euclid(m, n)
m, n = n, m if n > m
m, n = n, m % n while m % n != 0
n
end
O grafo gerado nesse caso é (clique para expandir):

Node que embora o número de nós e arestas tenha mudado, a relação entre eles não mudou e a complexidade permanece a mesma.
Testando
De uma forma geral, o valor da complexidade ciclomática define um limite superior para a quantidade de testes necessários para cobrir todos os caminhos decisórios no código em questão. Esse é um limite superior já que nem todos os caminhos são necessariamente realizáveis.
Disso se infere que quanto menor a complexidade, menor a quantidade de testes necessários para o método em questão. Esse fato implica em outro curioso: quebra um método em vários reduz a complexidade dos métodos mas aumenta a complexidade geral do código e, de forma geral, mantém a testabilidade do programa completo no mesmo nível.
Referências
Obviamente, já que a complexidade é um valor específico, é possível extrair da mesma uma referência. Baseado no trabalho de McCabe, esses valores de referência são:
- 1-10, métodos simples, sem muito risco
- 11-20, métodos medianamente complexos, com risco moderado
- 21-50, métodos complexos, com risco alto
- 51 ou mais, métodos instáveis de altíssimo risco
Conclusão
Essa foi uma pequena introdução ao assunto com o objetivo de abrir o caminho para artigos posteriores mostrando ferramentas de apoio ao cálculo e monitoramento da complexidade ciclomática. Como de usual, sugestões e correções são bem vindos.