Como testar uma preocupação do controlador no Rails 4
-
21-12-2019 - |
Pergunta
Qual é a melhor maneira de lidar com testes de preocupações quando usados em controladores Rails 4?Digamos que eu tenha uma preocupação trivial Citations
.
module Citations
extend ActiveSupport::Concern
def citations ; end
end
O comportamento esperado sob teste é que qualquer controlador que inclua esta preocupação receba esta citations
ponto final.
class ConversationController < ActionController::Base
include Citations
end
Simples.
ConversationController.new.respond_to? :yelling #=> true
Mas qual é a maneira correta de testar essa preocupação isoladamente?
class CitationConcernController < ActionController::Base
include Citations
end
describe CitationConcernController, type: :controller do
it 'should add the citations endpoint' do
get :citations
expect(response).to be_successful
end
end
Infelizmente, isso falha.
CitationConcernController
should add the citations endpoint (FAILED - 1)
Failures:
1) CitationConcernController should add the citations endpoint
Failure/Error: get :citations
ActionController::UrlGenerationError:
No route matches {:controller=>"citation_concern", :action=>"citations"}
# ./controller_concern_spec.rb:14:in `block (2 levels) in <top (required)>'
Este é um exemplo inventado.No meu aplicativo, recebo um erro diferente.
RuntimeError:
@routes is nil: make sure you set it in your test's setup method.
Solução
Você encontrará muitos conselhos para usar exemplos compartilhados e executá-los no escopo dos controladores incluídos.
Pessoalmente, acho isso um exagero e prefiro realizar testes unitários isoladamente e depois usar testes de integração para confirmar o comportamento dos meus controladores.
Método 1:sem roteamento ou teste de resposta
Crie um controlador falso e teste seus métodos:
describe MyControllerConcern do
before do
class FakesController < ApplicationController
include MyControllerConcern
end
end
after { Object.send :remove_const, :FakesController }
let(:object) { FakesController.new }
describe 'my_method_to_test' do
it { expect(object).to eq('expected result') }
end
end
Método 2:resposta de teste
Quando sua preocupação contém roteamento ou você precisa testar resposta, renderização, etc.você precisa executar seu teste com controlador anônimo.Isso permite que você obtenha acesso a todos os métodos e auxiliares rspec relacionados ao controlador:
describe MyControllerConcern, type: :controller do
controller(ApplicationController) do
include MyControllerConcern
def fake_action; redirect_to '/an_url'; end
end
before { routes.draw {
get 'fake_action' => 'anonymous#fake_action'
} }
describe 'my_method_to_test' do
before { get :fake_action }
it { expect(response).to redirect_to('/an_url') }
end
end
Você pode ver que temos que envolver o controlador anônimo em um controller(ApplicationController)
.Se suas classes forem herdadas de outra classe que não ApplicationController
, você precisará adaptar isso.
Além disso, para que isso funcione corretamente, você deve declarar em seu spec_helper.rb arquivo:
config.infer_base_class_for_anonymous_controllers = true
Observação:continue testando se sua preocupação está incluída
Também é importante testar se sua classe de interesse está incluída em suas classes alvo, uma linha é suficiente:
describe SomeTargetedController do
describe 'includes MyControllerConcern' do
it { expect(SomeTargetedController.ancestors.include? MyControllerConcern).to eq(true) }
end
end
Outras dicas
Simplificando o método 2 da resposta mais votada.
Eu prefiro o anonymous controller
suportado em rspec http://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/anonymous-controller
Você vai fazer:
describe ApplicationController, type: :controller do
controller do
include MyControllerConcern
def index; end
end
describe 'GET index' do
it 'will work' do
get :index
end
end
end
Observe que você precisa descrever o ApplicationController
e defina o tipo caso isso não aconteça por padrão.
Minha resposta pode parecer um pouco mais complicada do que a de @Benj e @Calin, mas tem suas vantagens.
describe Concerns::MyConcern, type: :controller do
described_class.tap do |mod|
controller(ActionController::Base) { include mod }
end
# your tests go here
end
Primeiramente, recomendo o uso do controlador anônimo que é uma subclasse de ActionController::Base
, não ApplicationController
nem qualquer outro controlador base definido em seu aplicativo.Dessa forma, você poderá testar a preocupação isoladamente de qualquer um dos seus controladores.Se você espera que alguns métodos sejam definidos em um controlador base, basta fazer um stub deles.
Além disso, é uma boa ideia evitar redigitar o nome do módulo de preocupação, pois isso ajuda a evitar erros de copiar e colar.Infelizmente, described_class
não está acessível em um bloco passado para controller(ActionController::Base)
, então eu uso #tap
método para criar outra ligação que armazena described_class
em uma variável local.Isto é especialmente importante ao trabalhar com APIs versionadas.Nesse caso, é bastante comum copiar grandes volumes de controladores ao criar uma nova versão, e é terrivelmente fácil cometer um erro tão sutil de copiar e colar.
Estou usando uma maneira mais simples de testar minhas preocupações com o controlador, não tenho certeza se essa é a maneira correta, mas parecia muito mais simples que a acima e faz sentido para mim, é como usar o escopo dos controladores incluídos.Por favor, deixe-me saber se houver algum problema com este método.controlador de amostra:
class MyController < BaseController
include MyConcern
def index
...
type = column_type(column_name)
...
end
fim
minha preocupação com o controlador:
module MyConcern
...
def column_type(name)
return :phone if (column =~ /phone/).present?
return :id if column == 'id' || (column =~ /_id/).present?
:default
end
...
end
teste de especificação para preocupação:
require 'spec_helper'
describe SearchFilter do
let(:ac) { MyController.new }
context '#column_type' do
it 'should return :phone for phone type column' do
expect(ac.column_type('phone')).to eq(:phone)
end
it 'should return :id for id column' do
expect(ac.column_type('company_id')).to eq(:id)
end
it 'should return :id for id column' do
expect(ac.column_type('id')).to eq(:id)
end
it 'should return :default for other types of columns' do
expect(ac.column_type('company_name')).to eq(:default)
end
end
end