Skip to content
setembro 10, 2009 / cassiomarques

Testando sweepers com Rspec

O problema

Quando usamos caching no Rails, precisamos criar também algum mecanismo para expirar esses caches quando os mesmos não forem mais válidos. Geralmente um cache não é mais válido quando o recurso exibido teve seu estado alterado na aplicação. Por exemplo: Você tem um blog e usa caching para não ter que ir ao banco toda vez que os posts forem exibidos. Entretanto, se um post for atualizado, seu respectivo cache deve ser expirado, caso contrário as alterações nunca serão visualizadas.

Um mecanismo bastante utilizado para expirar caches são os sweepers. Sweepers são objetos compartilhados, muito semelhantes aos observers do ActiveRecord (na verdade um Sweeper é uma subclasse de Observer), que podem ser registrados nos controllers de forma semelhante a filtros, para observar alterações em models e expirar os respectivos caches.

O problema que eu tive foi que queria testar um sweeper com meu querido Rspec. Mais especificamente, um sweeper que expirava um fragment cache, que é um cache para apenas um pedaço da minha página. Não achei nada pronto pra isso, nenhuma facilidade. Resolvi coçar minha própria coceira e criar alguma coisa que pudesse me ajudar.

Não vou explicar aqui como funciona caching no Rails, nem como usar Sweepers. Você pode pesquisar mais sobre isso aqui.

Vamos seguir utilizando o velho e muito mais que manjado exemplo da Blog App. Vamos considerar que a página inicial da nossa app possui uma área com os posts mais recentes, sobre a qual foi aplicado fragment caching. Para expirar este cache, poderiamos ter um sweeper mais ou menos assim:

class PostSweeper < ActionController::Caching::Sweeper
  observe Post

  def expire_post(post)
    expire_fragment "post_#{post.id}"
  end

  alias_method :after_save, :expire_post
  alias_method :after_destroy, :expire_post
end
&#91;/source&#93;

Como testar isso? Como saber que o fragmento correto é expirado quando alteramos o registro no banco? 

<h2>A solução</h2>
Para testar esse sweeper, criei um controller "de teste", sem utilidade para a aplicação. Porque eu não usei um controller já existente? Porque eu não queria criar dependência entre os testes. Não gosto da idéia de ter que depender de uma classe diferente da que está sendo testada para que meu teste passe, pelo menos não se eu puder evitar.
Usei um nome diferente de PostsController, para não redefinir a classe durante os testes, isso poderia causar problemas. O código abaixo pode ser definido no próprio arquivo da spec, logo acima do seu bloco <strong>describe</strong> principal, por exemplo.


class FooBarsController < ApplicationController
  cache_sweeper :post_sweeper

  def update 
    @post = Post.find(params&#91;:id&#93;)
    @post.update_attributes :contents, "hello world!"
  end

  def destroy
    @post = Post.find(params&#91;:id&#93;)
    @post.destroy
  end
end

describe PostSweeper do
  #...
end
&#91;/source&#93;

O próximo passo é criar uma forma de habilitar o caching em nossos testes. Por padrão ele fica desabilitado no ambiente de teste, para tornar tudo mais rápido. Vamos ter que habilitá-lo se quisermos que nosso teste funcione. Aqui habilito o caching somente para a spec em questão, sem alterar o comportamento para os demais testes da nossa aplicação. Para isso, criei um helper:

&#91;source language="ruby"&#93;
def performing_cache
  ActionController::Base.perform_caching = true
  yield
  ActionController::Base.perform_caching = false  
end
&#91;/source&#93;

A idéia para o teste é invocar as actions do FooBarsController e ver se o cache é expirado quando o post é atualizado ou removido. Simples né? Hum... depende... como vamos invocar as actions? Como este é uma spec para o sweeper e não para o controller, não podemos simplesmente fazer algo como 

&#91;source language="ruby"&#93;
  put :update, :id => "1"

como fariamos em um teste de controller. Nossa spec não saberia qual controller é dono da action update em questão. Vamos precisar cavar um pouco e descobrir outra forma:

describe PostSweeper do
  before :all do
    @app = ActionController::Integration::Session.new
    ActionController::Routing::Routes.draw do |map|
      map.resources :foo_bars, :only => [:update, :destroy]
    end    
  end

  #...
end

No código acima criamos a variável de instância @app. Ela representa uma app Rails, sobre a qual podemos invocar actions. Criamos também uma rota para nosso controller, de forma a podermos usar URIs para as actions.

Agora precisamos criar um matcher do Rspec para verificar se o fragmento está sendo devidamente expirado. Fiz de forma bem simples, poderia ser melhorado. Basicamente o que faço aqui é verificar se o cache store do controller de teste recebe uma mensagem pedindo para que o fragment cache seja expirado.

Spec::Matchers.define :expire_fragment do |fragment, options|
  match do |controller|
    controller.cache_store.should_receive(:delete).with("views/#{fragment}", options)
  end
end

Ok, precisei dar uma fuçadinha no código do Rails para descobrir como fazer isso :)

Agora juntando tudo, podemos escrever uma spec assim:

describe PostSweeper do
  before :all do
    @app = ActionController::Integration::Session.new
    ActionController::Routing::Routes.draw do |map|
      map.resources :foo_bars, :only => [:update, :destroy]
    end    
  end
  
  before :each do
    @post = Post.create(:contents => "blablabla")
  end

  it "deve limpar o cache quando o post for atualizado" do
    FooBarsController.should expire_fragment("post_#{@post.id}", nil)
    performing_cache do
      @app.put("/foo_bars/#{@post.id}")
    end
  end

  it "deve limpar o cache quando o post for removido" do
    FooBarsController.should expire_fragment("post_#{@post.id}", nil)
    performing_cache do
      @app.delete("/foo_bars/#{@post.id}")
    end
  end
end

Precisamos passar nil como segundo argumento para o matcher, porque ali iriam as opções para expirar o cache, as quais não estamos usando. Preciso pensar em uma forma melhor de fazer isso depois, para passar um segundo argumento somente quando ele for utilizado. Com Ruby 1.9 isso poderia ser resolvido facilmente, pois argumentos de blocos aceitam valores default, como acontece com argumentos de métodos.

5 Comentários

Deixe um comentário
  1. Daniel / set 11 2009 12:07 am

    Muito bom mesmo…. mas eu também não acho que seria incorreto testar se os sweepers estão funcionando pelo spec do controller.

    Porque pensando de uma forma mais macro, expirar o cache é um comportamento que pertence ao controller (mesmo que seja através de um “observer”) então não acho que estaria errado só checar se o cache_store foi chamado direto no spec do controller.

    Bem, é a minha opinião.

    • cassiomarques / set 11 2009 12:30 am

      Fala Daniel,

      Realmente não seria errado testar no controller, concordo com você. É que no meu caso eu nunca uso objetos reais nos testes de controller, faço mocking de todos os models e com isso não conseguiria fazer com que o sweeper fosse realmente acionado.

      O ideal seria criar uma forma bacana de testar no controller também e usar os dois testes em conjunto :)

      Abraço!

  2. Daniel / set 11 2009 9:17 pm

    Cara, vc usa mocha? To preso no mocha e acho ele péssimo para criar mocks de models com associações aí acabo usando Factory nos controllers o que é bem feio.

    Se usar mocha, como vc fazer seus mocks de models que tem association proxy?

    • cassiomarques / set 11 2009 9:48 pm

      Eu não uso o mocha, uso o esquema de mocking do próprio Rspec mesmo. Com ele, quando preciso mockar/stubar alguma associação, faço o de sempre mesmo…

      se Foo has_many :bars, então

      @foo.stub!(:bars).and_return([@bar1, @bar2, …])

      e os mocks de forma análoga, com should_receive

  3. Daniel / set 11 2009 10:21 pm

    é… eu prefiro os mocks do RSpec exatamente pq quando faça um mock_model e passo o nome da associação ele moca os id’s direitinho e tudo no mocha tem que stubar tudo na mão.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: