#controllers/concerns/response.rb
module Response
extend ActiveSupport::Concern
def json_response(object, status = :ok, opts = {})
response = {json: object, status: status}.merge(opts)
render response
end
def respond_with_errors(object)
render json: { errors: ErrorSerializer.serialize(object) }, status: :unprocessable_entity
end
def paginated_response_status(collection)
collection.size > WillPaginate.per_page ? :partial_content : :ok
end
endclass ApplicationController < ActionController::API
include Response
...
endHiA concern is just a bunch of methods you include into a class. You can test them either independently by bringing the concern into a plain old class, or if you need to use controller methods you can bring them into a controller. There is a anonymous controller in controller specs you can use for this purpose.However the Rails team have deprecated controller tests (and therefore controller specs) in favour of request specs, because the way that Rails controllers operate are not well suited towards unit tests (they are always a form of integration test due to the way that the Rails stack was designed).So it depends on what you are testing.
#spec/controllers/concerns/response_spec.rb
require 'rails_helper'
class FakeController < ApplicationController
end
RSpec.describe Response do
end
If you need a controller you need a controller spec, its not practical to instantiate a controller on your own, one of the many reasons why they are recommended against by the Rails team now.Otherwise you need to test the behaviour of the end result, e.g. create a set of shared examples for your concern and use them in every request/system/integration test for the routes concerned.
#spec/controllers/concerns/response_spec.rb
require 'rails_helper'
class FakeController < ApplicationController
end
RSpec.describe FakeController, type: :controller do
let(:controller) { FakeController.new}
FakeModel = Struct.new(:name)
describe 'Response concern' do
context '#json_response' do
it 'renders JSON response' do
fake_model = FakeModel.new('example')
result = controller.json_response(fake_model)
puts "result: #{result.inspect}"
end
end
end
end
rspec spec/controllers/concerns/response_spec.rb
F
Failures:
1) FakeController Response concern #json_response renders JSON response
Failure/Error: render response
Module::DelegationError:
ActionController::Metal#status= delegated to @_response.status=, but @_response is nil: #<FakeController:0x00007fd004810700 @_routes=nil, @_request=nil, @_response=nil, @_config={}, @_db_runtime=109.12200000000001>
# ./app/controllers/concerns/response.rb:6:in `json_response'
# ./spec/controllers/concerns/response_spec.rb:14:in `block (4 levels) in <top (required)>'
# ------------------
# --- Caused by: ---
# NoMethodError:
# undefined method `status=' for nil:NilClass
# ./app/controllers/concerns/response.rb:6:in `json_response'
Finished in 0.17825 seconds (files took 1.12 seconds to load)
1 example, 1 failure
#spec/shared/json_response.rb
require 'rails_helper'
RSpec.shared_examples 'JSON Responsive controller' do |controller_class|
let(:controller_class) { including_class.new }
it 'render JSON response' do
expect(controller_class).to respond_to(:json_response)
end
end
#spec/controllers/concerns/fake_controller_spec.rb
require 'rails_helper'
class FakeController < ApplicationController
end
RSpec.describe FakeController, type: :controller do
it_behaves_like 'JSON Responsive controller', FakeController
end
Failures:
1) FakeController behaves like JSON Responsive class render JSON response
Failure/Error: expect(controller_class).to respond_to(:json_response)
expected FakeController to respond to :json_response
Shared Example Group: "JSON Responsive class" called from ./spec/controllers/concerns/fake_controller_spec.rb:7
# ./spec/shared/json_response.rb:7:in `block (2 levels) in <main>'
require 'rails_helper'
RSpec.shared_examples 'JSON Responsive controller' do
let(:instance) { described_class.new }
describe '#json_response' do
it 'should respond with JSON response' do
expect(instance).to respond_to(:json_response)
end
it 'returns correct JSON with default status' do
model = double(:model)
json = instance.json_response(model)
puts json.inspect
end
end
endModule::DelegationError:
ActionController::Metal#status= delegated to @_response.status=, but @_response is nil: #<FakeController:0x00007ff2f2013758 @_routes=nil, @_request=nil, @_response=nil, @_config={}, @_db_runtime=216.53>
Shared Example Group: "JSON Responsive controller" called from ./spec/controllers/concerns/fake_controller_spec.rb:7
# ./app/controllers/concerns/response.rb:6:in `json_response'
# ./spec/shared/json_response.rb:13:in `block (3 levels) in <main>'
You need to include your concern in your fake controller.We do have the anonymous controller helpers for this purpose. See: https://relishapp.com/rspec/rspec-rails/docs/controller-specs/anonymous-controllerAlthough you’ll still need to include the concern.
require 'rails_helper'
class FakeController < ApplicationController
end
RSpec.describe FakeController, type: :controller do
it_should_behave_like "JSON Responsive controller" do
let(:instance) { FakeController.new }
enddef json_response(object, status = :ok, opts = {})
response = {json: object, status: status}.merge(opts)
puts "++++++++ response: #{response.inspect}"
render response
end++++++++ response: {:json=>#<Double :model>, :status=>:ok}Module::DelegationError:
ActionController::Metal#status= delegated to @_response.status=, but @_response is nil: #<FakeController:0x00007faf08fe0920 @_routes=nil, @_request=nil, @_response=nil, @_config={}, @_db_runtime=181.55>
Shared Example Group: "JSON Responsive controller" called from ./spec/controllers/concerns/fake_controller_spec.rb:7
# ./app/controllers/concerns/response.rb:7:in `json_response'
# ./spec/shared/json_response.rb:13:in `block (3 levels) in <main>'
# ------------------
# --- Caused by: ---
# NoMethodError:
# undefined method `status=' for nil:NilClass
# ./app/controllers/concerns/response.rb:7:in `json_response'#spec/shared/json_response.rb
require 'rails_helper'
RSpec.shared_examples 'JSON Responsive controller' do |including_controller|
let(:instance) { including_controller.new }
it 'should respond to #json_response' do
expect(instance).to respond_to(:json_response)
end
it 'should respond #respond_with_errors' do
expect(instance).to respond_to(:respond_with_errors)
end
it 'should respond to #paginated_response_status' do
expect(instance).to respond_to(:paginated_response_status)
end
context '#paginated_response_status' do
it 'return 200 if collection is not paginated' do
expect(instance.paginated_response_status([1])).to eq :ok
end
it 'return 206 if collection is paginated' do
collection = (1..35).to_a
expect(instance.paginated_response_status(collection)).to eq :partial_content
end
end
endrequire 'rails_helper'
class FakeController < ApplicationController
def render(*args)
args.first
end
end
RSpec.describe FakeController, type: :controller do
it_should_behave_like "JSON Responsive controller", FakeController
endcontext '#respond_with_errors' do
it 'returns :unprocessable_entity status' do
model = double(:model)
errors = double(:errors, messages: {})
allow(model).to receive(:errors).and_return(errors)
response = instance.respond_with_errors(model)
expect(response[:status]).to eq :unprocessable_entity
end
end