Blog da Improve It

Como Testar parte 2 - Mocks

Publicado por Marcos Tapajós há mais de 5 anos.

Tem gente "chutando" Demeter.

Minha idéia era escrever um post dessa série por semana, mas infelizmente uma tendinite tem me atacado e está meio complicado ficar escrevendo muito. Por isso mesmo esse segundo artigo será bem compacto. Todos os códigos citados nesse post fazem parte de um projeto How Test que está disponível em: http://github.com/tapajos/how-test Nesse post a minha idéia é mostrar de forma bem simples como funciona um [mock object][mo] no Rspec e no Test::Unit(usando o mocha). Para exemplificar eu vou fazer uma crítica a uma construção que eu tenho visto muito em diversos projetos e que viola a [Lei de Demeter][ld](Principle of Least Knowledge). Ou como dizia, o meu amigo, Bernardo, "Tem gente chutando Demeter!". ###Vamos a um exemplo dessa violação: Supondo que você tenha um modelo Account que se relaciona ao modelo User. class Account < ActiveRecord::Base belongs_to :user end Freqüentemente eu vejo construções do tipo: @account.user.name @account.user.mail @account.user.rg.number @account.user.rg.state @account.user.rg.city O grande problema é que nesse tipo de construção você está "conhecendo" coisas demais e certamente vai pagar por isso num futuro breve, quando você precisar fazer um refactoring e tiver que mudar em vários lugares. Imagina se o User deixa de ter um "name" e passa a ter um "full\_name". Para resolver esse tipo de problema basta você concentrar esse "conhecimento" no seu modelo Account da seguinte forma: class Account < ActiveRecord::Base belongs_to :user def user_name user.name end end OBS: Vou me concentrar apenas no problema com o nome e não vou me preocupar em validar se o relacionamento foi estabelecido. Bem, finalizada a crítica a uma falha de design OO vamos ao objetivo desse post, mostrar como *EU* testaria esse problema. Como eu falei no post anterior, não gosto muito de usar fixtures para testes unitários e por isso mesmo vou apelar aos [mock objects][mo]. Se você não está muito familiarizado com mocks sugiro que pare por aqui e leia um pouco mais sobre isso. Uma referência rápida pode ser o [wikipedia][mo] mas realmente sugiro que vá mais adiante. Testando usando RSpec(usando o framework de mock padrão): before(:each) do @account = Account.new @user_mock = mock_model(User) @account.stub!(:user).and_return(@user_mock) end describe ".user_name" do it "should delegate to user.name" do @user_mock.should_receive(:name).and_return("Tapajós") @account.user_name.should == "Tapajós" end end Testando usando Test::Unit com mocha: setup :create_model def test_user_name @user_mock.expects(:name).returns("Tapajós") assert_equal "Tapajós", @account.user_name end private def create_model @account = Account.new @user_mock = mock("User") @account.stubs(:user).returns(@user_mock) end ###Explicando... Em ambos os casos eu preciso que a minha account simule o relacionamento com User e para isso eu vou retornar um mock object. Isso é feito pelas linhas: RSpec: @account.stub!(:user).and_return(@user_mock) Test::Unit: @account.stubs(:user).returns(@user_mock) Após o setup ou o before, temos um modelo @account onde o @account.user retorna um mock. Feito isso eu preciso configurar o meu mock, isto é, informar que ele receberá uma mensagem name (chamada do método .name) e essa retornará o meu nome(sim, sou egocêntrico). Isso é feito pelos métodos "should\_receive" e "expects" conforme as linhas abaixo: RSpec: @user_mock.should_receive(:name).and_return("Tapajós") Test::Unit: @user_mock.expects(:name).returns("Tapajós") Depois que nossos mocks foram devidamente configurados podemos, finalmente, fazer nossa verificação do retorno, isto é, simplesmente chamar nossos métodos conferir o retorno. Pronto teste feito com sucesso, sem precisar recorrer a banco de dados nem configurar fixtures. Nesse momento deve ter surgido uma dúvida: "Porque uma hora você usa stub! e outra um should\_receive?" A resposta é bem simples, o stub! não faz um verify no final enquanto a outra chamada sim. Na pratica isso significa: ### Stub! ou stubs Quando eu uso stub!(ou um Stubs) eu estou configurando o meu modelo account para responder pelo método user retornando o @mock\_user porém não me interessa quebrar esse teste caso você não chame esse método. ### Should\_receive ou expects Quando eu uso should\_receive(ou um expects) eu estou configurando o meu mock user para responder pelo método name porém caso esse método não seja chamado eu devo quebrar meu teste, pois isso seria um comportamento indesejável. ## Porque usar mocks? Ao contrário do que muita gente pensa o uso de mocks não é um bicho de 7 cabeças, é bem simples. Na verdade testar é algo simples, desde que você tenha domínio do ferramental e os mocks são realmente úteis em diversos casos. Imagina que o seu sistema precise fazer consultas a uma api publica do Yahoo e para isso você tenha criado uma classe de consultas. Você não vai querer(nem o Yahoo vai gostar) ir lá no servidor toda vez que você rodar os seus testes. Isso tornaria os seus testes lentos e impossível roda-los offline. Nesse caso você resolve seu problema "mockando" essa classe. [mo]: http://en.wikipedia.org/wiki/Mock_Object [ld]: http://en.wikipedia.org/wiki/Law_of_Demeter

Tags , , , , , ,  | 8 comentários

Como Testar parte 1 - Models

Publicado por Marcos Tapajós há mais de 5 anos.

Quando comecei a estudar [Extreme Programming][xp] descobri que não é possível fazer nenhum software de qualidade sem uma excelente base de testes. Desde então tenho me dedicado muito ao estudo das mais diversas ferramentas e técnicas para elaborar bons de testes. O assunto testes é bastante polêmico e não pretendo (nesse post) tentar convencer ninguém da importância deles. Se você não faz testes e/ou discorda de qualquer uma das minhas afirmações deixo algumas perguntas para você refletir. 1 - Quantos bugs fixes você fez esse ano? 2 - Quantos tickets abertos existem no seu bug tracker? 3 - Quantas vezes você fez um deploy de uma nova versão em uma sexta feira de tarde e saiu mais cedo do trabalho? 4 - Quantas vezes você "virou a noite" esse ano? Acabei me tornando um evangelizador de testes porém nunca fiz nada muito prático para passar o conhecimento que eu adquiri para a comunidade. Só que agora vou me redimir dessa falha iniciando uma série de posts onde vou expor um problema e como EU testaria usando Test::Unit e Rspec. Não vou falar de Shoulda pois não gosto dele. :-) A idéia de escrever essa série de posts sobre testes surgiu logo após a gravação do terceiro episódio do [RailsBox][rb] e gostaria de agradecer ao Ozeias e ao Davis Cabral por terem me motivado. "Back to the cold cow..." O ActiveRecord simplifica muito nossos modelos porém tenho observado que em vários projetos os desenvolvedores deixam de testar corretamente os seus modelos usando a alegação que não vão testar alguma coisa que o Rails já testou. Esse é um argumento valido em alguns casos pois você está apenas delegando responsabilidades mas você sempre deve testar se a responsabilidade foi realmente delegada. Um exemplo clássico são as validações. Teoricamente você não precisaria testar como elas são implementadas mas deve testar se elas realmente existem pois se alguém remove-las seus testes vão continuar passando mas sua aplicação estará quebrada e/ou permitindo inconsistências de banco de dados. Nesse post vou mostrar como testar alguns comportamento do ActiveRecord usando como base o modelo User. Todos os códigos citados nesse post fazem parte de um projeto How Test que está disponível em: http://github.com/tapajos/how-test class User < ActiveRecord::Base validates_presence_of :name validates_format_of :mail, :with => /([-\.\w^@]+@(?:[-\w]+\.)+[A-Za-z]{2,4})+/i, :on => :create, :allow_nil => true has_many :accounts named_scope :actives, :conditions => ["active = ?", true] end Uma boa estratégia para orientar o desenvolvimento dos teste é elaborar algumas perguntas que darão origem aos seus cenários de testes. ### Validação do nome 1. Posso cadastrar um usuário sem nome? Não #### Testando usando Test:Unit: def test_if_check_presence_of_name assert !@user.valid?, "Should be invalid" assert_equal "can't be blank", @user.errors[:name] end Na primeira linha desse teste o assert recebe um segundo parâmetro que por ser opcional não é muito comentado mas merece uma atenção especial. Esse argumento nada mais é do que a mensagem que será exibida quando o teste quebrar. Quando você omite esse parâmetro o teste quebra exibindo a mensagem 'false is not true' que não ajuda muito a entender o que está acontecendo. #### Testando usando RSpec: it "should reject if name is not given" do @user.should have(1).error_on(:name) @user.errors[:name].should == "can't be blank" end ### Validação do e-mail. 1. Posso criar um registro com um e-mail inválido? Não 2. Posso atualizar um regitro com um e-mail inválido? Sim 3. Posso criar um registro com um e-mail em branco? Sim #### Testando usando Test:Unit: VALIDS_MAIL = %w(foo@bar.com foo@bar.com.br foo@globo.com foo@i_hate_the_microsoft.com foo@i_love_my_mac.com) INVALIDS_MAIL = %w(foobar.com foo@bar i_hate_the_microsoft.com i_love_my_mac.com) def test_if_reject_invalid_format_os_mail_on_create INVALIDS_MAIL.each do |mail| @user.mail = mail assert !@user.valid?, "Should be invalid when mail is #{mail}" assert_equal "is invalid", @user.errors[:mail] end end def test_if_not_reject_when_mail_is_nil @user.name = "Tapajós" assert @user.valid?, "Should be valid" end def test_if_not_check_format_of_mail_on_update @user.name = "Tapajós" assert @user.save, "Should save" @user.mail = "an invalid mail" assert @user.valid?, "Should be valid" end def test_if_accept_a_valid_mail VALIDS_MAIL.each do |mail| @user.name = "Tapajós" @user.mail = mail assert @user.valid?, "Should be valid when mail is #{mail}" end end #### Testando usando RSpec: INVALIDS_MAIL.each do |mail| it "should reject because #{mail} is an invalid mail" do @user.mail = mail @user.should have(1).error_on(:mail) @user.errors[:mail].should == "is invalid" end end VALIDS_MAIL.each do |mail| it "should be valid when mail is #{mail}" do @user.mail = mail @user.should_not have(1).error_on(:mail) end end it "should not reject if mail is not given" do @user.name = "Tapajós" @user.should be_valid end it "should not check mail format on update" do @user.name = "Tapajós" @user.save.should be_true @user.mail = "an invalid mail" @user.should be_valid end ### Testando o relacionamento com Account. 1. Um usuário pode ter mais de uma conta? Sim #### Testando usando Test::Unit: def test_has_many_accounts association = User.reflect_on_association(:accounts) assert association, "Association with account is not found" assert_equal :has_many, association.macro end #### Testando usando RSpec: it "should has many accounts" do association = User.reflect_on_association(:accounts) association.should_not be_nil association.macro.should == :has_many end ### Testando o User.actives 1. Posso listar usuários inativos? Não #### Testando usando Test::Unit: def test_if_actives_use_the_correct_conditions assert_equal({:conditions=>["active = ?", true]}, User.actives.proxy_options) end #### Testando usando RSpec: it "should find for all users that status of active is true" do User.actives.proxy_options.should == {:conditions=>["active = ?", true]} end Para esse post ficar mais simples e curto não me preocupei em validar se o tamanho máximo dos campos está coerente com o tamanho máximo permitido pelo tipo no banco de dados. Essa é uma validação EXTREMAMENTE importante que não deve ser esquecida! No próximo post dessa série falarei um pouco sobre a [Lei de Demeter][ld], como respeita-la e testar alguns métodos usando [Mock Objects][m]. O que você achou desse artigo? O que você gostaria de saber sobre testes de ActiveRecord que eu não falei aqui? Aguardo o feedback de vocês. [m]: http://en.wikipedia.org/wiki/Mock_Object [rb]: http://railsbox.org/2008/9/9/railsbox-podcast-3 [xp]: http://www.improveit.com.br/xp [ld]: http://en.wikipedia.org/wiki/Law_of_Demeter

Tags , , , , , ,  | 46 comentários

Plugin Brazilian Rails atualizado.

Publicado por Marcos Tapajós há mais de 6 anos.

O [Eduardo Fiorezi][ed] e o Bruno Iecker identificaram um [bug][] no [Brazilian Rails][br] e me enviaram um patch só que devido a minha falta de tempo ainda não tinha verificado. Acabei de aplicar o patch e como os outros que eles enviaram está tudo muito bem feito e testado. Gostaria de pedir desculpas pela demora e agradecer novamente aos dois pela ajuda. [ed]:http://tudoquequerosaber.com/ [bug]: http://rubyforge.org/tracker/index.php?func=detail&aid=16897&group_id=4003&atid=15412 [br]: http://brazilianrails.improveit.com.br

Tags , , ,  | 5 comentários

Plugin Selenium on Rails no Rails 2.0

Publicado por Marcos Tapajós há mais de 6 anos.

Hoje fui migrar um sistema para o Rails 2.0.1 e tudo estava funcionando mas quando fui rodar os testes do Selenium alguns quebraram pois as fixtures não eram carregadas no banco de testes. Descobri que estava relacionado a um cache de fixtures e consegui resolver o problema. Acabei de enviar um email para o autor do plugin sugerindo um patch. Como não sei quanto tempo vai demorar para sair a correção então resolvi publicar a minha solução aqui para ajudar quem esbarrar nesse problema. Para resolver o problema basta editar o arquivo fixture_loader.rb e modificar o trecho: if fixtures.any? Fixtures.create_fixtures fixtures_path, fixtures end Para: if fixtures.any? Fixtures.reset_cache Fixtures.create_fixtures fixtures_path, fixtures end

Tags , , ,  | 3 comentários

Podcast "Tudo que quero saber"

Publicado por Marcos Tapajós há mais de 6 anos.

[Eduardo Fiorezi][t] publicou mais um [podcast][] da série ["Tudo que quero saber!"][m]. Dessa vez foi sobre disciplina em projetos com [eXtreme Programming][XP] e [eu][] fui entrevistado junto com o [Danilo Sato][sato]. Fiquei muito feliz com o convite do [Eduardo][t] e em conhecer o Danilo, com quem já troquei algumas figurinhas sobre o [Dojo-SP][dsp]. Obrigado [Eduardo][t] ! Mais informações sobre o [podcast][] [aqui][m]. [dsp]: http://groups.google.com/group/dojo_sp [sato]: http://www.dtsato.com/ [XP]: http://www.improveit.com.br/xp [t]: http://tudoquequerosaber.com [podcast]: http://podcasts.tudoquequerosaber.com/podcast/Pod10-Disciplina_em_XP_com_Danilo_Sato_e_Marcos_Tapajos.mp3 [eu]: http://www.improveit.com.br/tapajos [m]: http://tudoquequerosaber.com/?p=40

Tags , , , , , , , , , , ,  | 2 comentários

Plugin Brazilian Rails

Publicado por Marcos Tapajós há mais de 6 anos.

Acabei de publicar no [RubyForge][rf] a primeira versão do plugin [Brazilian Rails][br]. Desde que eu comecei a programar em [Ruby][], usando o [Ruby on Rails][rails], sempre adorei o sistema de plugins, onde facilmente podemos incorporar novas funcionalidades que atendem a vários projetos. Sempre tive vontade de colocar nossos códigos disponíveis como plugins mas nunca tive tempo para fazer isso. Inevitavelmente continuo sem tempo mas agora estou contando com um time de amigos que resolveram me ajudar. São eles: * André Luiz Kupkovski ([Ancar][]) * Celestino Ferreira Gomes ([Ancar][]) * Rafael Fraga Walter ([Ancar][]) * Fernando João Manfroi ([Ancar][]) * Luciene Souza Luna ([Ancar][]) Esse plugin surgiu da necessidade de usar o método error___messages_for para sinalizar na camada de vista os erros encontrados nas validações do nosso modelo. Essas mensagens eram em inglês, o que fazia com que os desenvolvedores tivessem que implementar algo semelhante no RHTML. Acabamos notando que várias outras coisas poderiam ficar mais simples aos brasileiros usando-as como estamos acostumados. Por exemplo, nosso formato padrão de data é DD/MM/AAAA mas [Ruby][] não trabalha da mesma forma. Para solucionar esse problema fizemos uma implementação que modifica o método to_date do modulo String para lidar com esse nosso formato. Esse nosso primeiro release não abrange todas as nossas implementações mas resolvemos publicar o quanto antes para poder contar com o [feedback][] dos nossos usuários e melhorar continuamente. Trata-se de um projeto Open Source, onde gostaríamos de contar com a colaboração da comunidade Brasileira com sugestões e quem sabe com novos desenvolvedores. Minha única exigência com relação aos patches é que eles venham acompanhados de [testes][]. Quem quiser experimentar nosso plugin basta executar uma única linha e reiniciar sua aplicação [Rails][]. script/plugin install -x \ svn://rubyforge.org/var/svn/brazilian-rails Como temos vários tipos de códigos diferentes devemos fazer outros plugins em breve ! [testes]: http://www.improveit.com.br/xp/praticas/tdd [rf]:http://www.rubyforge.org [br]:http://rubyforge.org/projects/brazilian-rails/ [ruby]: http://www.ruby-lang.org [rails]: http://www.rubyonrails.org/ [ancar]: http://www.ancar.com.br [feedback]: http://www.improveit.com.br/xp/valores/feedback

Tags , , , , , , , , ,  | 16 comentários