quinta-feira, 26 de fevereiro de 2009

Duck typing e os testes de aceitação automáticos

Uma discussão frequente em minha equipe é sobre maneiras de garantir que nossos sistemas estejam funcionando adequadamente por meio de testes automáticos. Em nossos ultimos projetos temos conseguido alcançar 100% de cobertura de testes unitários, e nos sistemas legados temos aumentado a cobertura aos poucos.

No entanto, estamos cientes de que nem 100% de cobertura de testes unitários garantem a ausência de bugs. Isso nos remete aos testes de aceitação automáticos, não só por causa do funcionamento das telas e fluxos de navegação, mas também dos possíveis problemas que um refactoring com um pouco menos de atenção pode causar em uma linguagem baseada em duck typing.

Darei um exemplo em ruby para ilustrar um bug que pode ser introduzido sem que seus testes unitários percebam. Digamos que você tenha as seguintes classes:

class Filme < ActiveRecord::Base do
def alugar
self.estoque.remover self
end
end
class Estoque < ActiveRecord::Base do
def remover(filme)
# ...
end
end

No seu spec que verifica o método alugar da classe Filme você colocaria um mock mais ou menos como mostrado abaixo:

@estoque = mock_model(Estoque)
@estoque.should_receive(:remover).with(@filme).once

Um dia você percebe que o nome do método remover não está explicando muito bem o significado, e resolve fazer um refactoring renomeando-o para remover_fita_do_filme.

Você altera os testes da classe Estoque, renomeia o método e logo depois recebe uma ligação de uma empresa de telefonia te oferecendo serviços excepcionais que você nunca precisou. Puto da vida você desliga o telefone, roda os testes e todos passam! Serviço feito!

Perceberam o problema? Sem um teste de aceitação automático ou no mínimo um teste manual, você não perceberia que a classe Filme continua referenciando o método antigo, remover, da classe Estoque. Esse tipo de problema não passaria em uma linguagem que não segue duck typing, pois a etapa de verificação de código da compilação serviria como uma espécie de teste unitário de tipos e nomes.

Um problema mais evidente seria um método que recebe um objeto que precisa ter determinado método, ou seja implementar determinada interface. Veja o exemplo abaixo, que mostra que ao alugar um item, no caso um Filme, adiciona-se todas as tags dele às tags preferenciais do cliente:

class Cliente < ActiveRecord::Base do
def alugar(item)
#...
adiciona_tags_preferenciais item.tags
end
end
class Filme < ActiveRecord::Base do
def tags
# ...
end
end

Se mudarmos o nome do método tags em Filme e também corrigirmos o uso de dele no método alugar da classe Cliente tudo funcionará bem. Mas, como usamos duck typing não temos como garantir que outros objetos, digamos alugáveis, tenham sido alterados. Se por exemplo esta locadora também alugas livros, teríamos um erro evidente:

class Cliente < ActiveRecord::Base do
def alugar(item)
#...
adiciona_tags_preferenciais item.tags_principais
end
end
class Filme < ActiveRecord::Base do
def tags_principais
# ...
end
end
class Livro < ActiveRecord::Base do
def tags
# ...
end
end

Esse tipo de erro só seria pego num teste manual se lembrássemos de testar o aluguel de livros também. Por isso é tão importante o teste de aceitação automático em uma linguagem com duck typing. Em Java por exemplo, que a interface precisa ser explícita, o erro seria pego no que poderíamos considerar o teste unitário que a compilação executa. As classes Filme e Livro implementariam a interface Alugavel por exemplo:

public interface Alugavel {
List tagsPrincipais();
}

Mas é claro que o uso de uma liguagem menos dinâmica não remove a necessidade de testes de aceitação automáticos. O valor desse tipo de testes é independente da linguagem, e sua implementação é importantíssima para garantir fluxos de navegação e integração entre os diversos componentes do sistema.

quinta-feira, 19 de fevereiro de 2009

Plugin do rails para copiar erros de um model para outro

Em um projeto pessoal precisei desenvolver uma maneira de copiar os erros de um model para outro. Como é uma funcionalidade que outrora já havia precisado, aproveitei para criar um plugin e disponibilizá-lo para quem mais tiver esse mesmo problema.

O plugin está disponível no GitHub pelo endereço http://github.com/timotta/copy_errors_from/tree/master. Para instalar no seu projeto basta rodar a seguinte linha:

script/plugin install git://github.com/timotta/copy_errors_from.git

Após instalar todos os seus models terão o método copy_errors_from, que pode ser utilizado como mostrado abaixo:

> filme = Filme.new :titulo => 'Corra que a polícia vem aí'
> ator = Ator.new
> filme.atores.push ator
> filme.save #return false
> filme.errors.entries #return []
> ator.errors.entries #return [['nome','Não pode ser vazio']]
> filme.copy_errors_from ator
> filme.errors.entries #return [['ator_nome','Não pode ser vazio']]
> filme.errors.on(:ator_nome) #return 'Não pode ser vazia'

sexta-feira, 23 de janeiro de 2009

Cuidado ao cachear named_scope no Rails

Deve-se tomar cuidado ao cachear o acesso a um named_scope pois ele tem um carregamento de forma preguiçosa, o famoso lazy load. Assim, o código abaixo gravaria no cache não os atores femininos, mas sim todos os atores.

class Filme
#...
def atores_femininos_cacheado
Rails.cache.fetch("#{self.id}:atores-principais") { self.atores.femininos }
end

class Ator
#...
named_scope :femininos, :conditions => { :sexo => 'F' }

Se você observar o log, verá que o rails executará a query sem as condições definidas na classe Ator para o named_scope :femininos. E que após ser cacheado, será essa a query que deixará de ser executada.

A query para buscar atores femininos do named_scope só será realmente executada quando algum método do objeto retornado pelo método .atores.femininos for chamado. Veja no exemplo abaixo:

@filme = Filme.find_by_id 10
filme.atores_femininos_cacheado.last

Ou seja, o cacheamento não está servindo ao propósito definido. Uma solução para isso é cachear diretamente o resultado de um find, como pode ser visto abaixo:

class Filme
#...
def atores_femininos_cacheado
Rails.cache.fetch("#{self.id}:atores-principais") do
Ator.find_all_by_filme_id self, :conditions => { :sexo => 'F' }
end
end

quarta-feira, 21 de janeiro de 2009

PGError com Rails no Heroku

Essa dica é para quem utiliza o Heroku Garden para desenvolver suas aplicações rails. Ao rodar os specs diretamente da interface web do Heroku pode ser que um dia você se depare com o seguinte erro:

PGError: ERROR: relation "filmes" does not exist
: SELECT a.attname, format_type(a.atttypid, a.atttypmod),
d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = 'filmes'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum

O erro acontece porque o link para rodar as migrations, que aparece na interface do heroku, não as executa no ambiente de testes. Portanto, é preciso abrir a tela do rake e executar as seguintes linhas antes de rodar seus specs.

db:migrate
db:test:prepare

UPDATE

Descobri outro problema do Heroku. O schema.rb é criado e alterado no diretório tmp. Dessa forma para que o PGError não dê novamente em outra atualização do banco, você precisa copiá-lo para o diretório db e então no console do rake rodar:

db:test:clone

quinta-feira, 9 de outubro de 2008

Imagem de fundo no captcha gerado pelo simple_captcha

O simple_captcha, plugin de geração de captcha para rails, não possui opção para colocar um imagem de fundo na imagem gerada. Mas fazer isso não é nenhum bicho de sete cabeças, basta alterar algumas partes chaves do plugin utilizando um pouco de rmagick. Vou mostrar aqui mais ou menos como fiz isso em um recente projeto.

No arquivo simple_captcha_image.rb basta alterar o método generate_simple_captcha_image para que na criação da imagem seja colocado o fundo desejado. Veja o exemplo de código abaixo:

def generate_simple_captcha_image(options={})  #:nodoc

fundo = Magick::Image.read(url_da_imagem_de_fundo).first
preenchimento = Magick::TextureFill.new(fundo)

@image = Magick::Image.new(197, 45, preenchimento) do
self.format = 'JPG'
end

# ...

Repare que existe ali uma chamada ao método url_da_imagem_de_fundo que no caso é um método que pega o path de uma imagem aleatória. Dessa forma o captcha poderá ter vários fundos diferentes. Veja um exemplo de como pode ser a implementação desse método:

  def url_da_imagem_de_fundo
"#{RAILS_ROOT}/vendor/plugins/simple_captcha/assets/imgs/picture_" + (rand(9)+1).to_s + ".jpg"
end

Foi preciso também comentar a chamada ao método que aplica estilo e distorção à imagem, mas não sei bem se isso é necessário. Como fiz isso, precisei alterar o método append_simple_captcha_code que escreve o texto na imagem, de forma a utilizar diversos tamanhos, posições e fontes diferentes.

quarta-feira, 8 de outubro de 2008

Validação no lado cliente com form_remote_tag

Para fazer validações de formulário no lado cliente basta informar ao método form_tag ou ao form_for, utilizando o parâmetro :onsubmit, o código javascript que se deseja executar para validá-lo. Veja no exemplo:

  <% form_tag "/entrar", :onsubmit => 'return valida(this)' do %>
<h1>ok</h1>
<% end %>

Contudo para a tag form_remote_tag e form_remote_for não há a possibilidade de utilizar o parâmetro :onsubmit. Isso porque o formulário gerado pelo rails já possui a ação onsubmit definida com o código que fará o acesso remoto. Para colocar então sua validação antes deste código deve-se usar o parâmetro :before como é mostrado no exemplo:

  <% form_remote_tag :url => "/entrar", 
:update => 'meu_div',
:before => 'if( !valida(this) ) return false' do %>
<h1>ok</h1>
<% end %>

Repare que é importante que haja o "return false" no caso de não ter sido validado, caso contrário ele executará o restante do código colocado pelo rails no onsubmit.

quinta-feira, 2 de outubro de 2008

Impedindo redeclaração de objetos javascript

Ao importar duas vezes um mesmo arquivo .js em uma página, as funções, protótipos e objetos são redeclarados. Se for necessário que um determinado objeto mantenha seu estado durante as iterações assíncronas dessa página, perde-se este estado na segunta importação do arquivo .js.

Veja por exemplo o objeto abaixo, que nada mais é que um repositório de outros objetos. Tá ta ta, não é um exemplo que tenha tanta utilidade, mas vá lá, é só pra explicar o mecanismo.
var Repositorio = {
sequencia:0,
lista:[],
registrar:function(obj) {
var id = ++this.sequencia;
this.lista[ id ] = obj;
return id;
},
recuperar:function(id) {
return this.lista[ id ];
}
}
Agora estando esse código num arquivo por exemplo repositorio.js teríamos o problema relatado com o código a seguir:
<script src="repositorio.js"></script>
<script>
Repositorio.registrar( new Object() );
Repositorio.registrar( new Object() );
</script>
<script src="repositorio.js"></script>
<script>
alert( Repositorio.recuperar( 1 ) );
</script>
Para impedir essa redeclaração do objeto, e os possíveis erros que ela pode causar, basta tentar utilizá-lo dentro de um bloco try e fazer sua declaração dentro do bloco catch. Ou seja, ele só será declarado caso haja um erro na tentativa de usá-lo. Veja:
try{ Repositorio.existo(); }
catch(e) {
Repositorio = {
sequencia:0,
lista:[],
existo:function(){},
registrar:function(obj) {
var id = ++this.sequencia;
this.lista[ id ] = obj;
return id;
},
recuperar:function(id) {
return this.lista[ id ];
}
}
}
Mas aí você poderia me dizer: Pô pra que tudo isso, basta não importar duas vezes o javascript. Acontece que se você está criando uma biblioteca para ser distribuída para diversos sites e blogs, você não deve subestimar a "criatividade" dos utilizadores dela. Então, o melhor é se precaver.