Testing a controller's concern methods

44 views
Skip to first unread message

belgoros

unread,
Feb 27, 2019, 5:46:42 AM2/27/19
to rspec
I can't figure out the right way to test controller concern methods. What kind of spec should it be, - controller or other?
Here is a concern I'd like to test:

#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
end


The ApplicationController includes the above concern as follows:

#controllers/application_controller.rb

class ApplicationController < ActionController::API
  include
Response
...
end


Thank you!

Jon Rowe

unread,
Feb 27, 2019, 5:49:28 AM2/27/19
to rs...@googlegroups.com
Hi

A 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.

Cheers
Jon Rowe
---------------------------

belgoros

unread,
Feb 27, 2019, 6:00:15 AM2/27/19
to rspec


On Wednesday, 27 February 2019 11:49:28 UTC+1, Jon Rowe wrote:
Hi

A 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.


Thank you, Jon, for your response.
As I'd like to test just the methods defined in the above module (concern), as far as I understood, I could put a test no matter in which folder under spec directory.
For example:

#spec/controllers/concerns/response_spec.rb


require 'rails_helper'


class FakeController < ApplicationController
 
end


RSpec.describe Response do
 
end


So if it is OK, the above FakeController should include my concern methods. But how to test them ?

Jon Rowe

unread,
Feb 27, 2019, 6:02:12 AM2/27/19
to rs...@googlegroups.com
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.

Jon Rowe
---------------------------

belgoros

unread,
Feb 27, 2019, 8:08:53 AM2/27/19
to rspec


On Wednesday, 27 February 2019 12:02:12 UTC+1, Jon Rowe wrote:
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.

I tried it as follows:

#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



but it fails with:

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



What am I missing here ?

belgoros

unread,
Feb 27, 2019, 8:25:11 AM2/27/19
to rspec
I modified by creating a shared example as follows:

#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




Then by using it in a controller spec:
#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




But it fails as follows:

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>'

Jon Rowe

unread,
Feb 27, 2019, 10:02:23 AM2/27/19
to rs...@googlegroups.com
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-controller

Although you’ll still need to include the concern.

Cheers
Jon Rowe
---------------------------

belgoros

unread,
Feb 27, 2019, 10:02:33 AM2/27/19
to rspec
I have the first example passing after modifying the shared examples spec as follows:
#spec/shared/json_response.rb

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  
end


The problem is the failing 2d example where I mock a model to pass in the json_response method:

Module::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>'

belgoros

unread,
Feb 27, 2019, 10:05:18 AM2/27/19
to rspec


On Wednesday, 27 February 2019 16:02:23 UTC+1, Jon Rowe wrote:
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-controller

Although you’ll still need to include the concern.

Weird, I have it defined as follows without including the module Response as it was included in ApplicationController:

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 }
 
end

end 

belgoros

unread,
Feb 27, 2019, 10:19:16 AM2/27/19
to rspec
The error is due the call render response in Response module:
def json_response(object, status = :ok, opts = {})
    response
= {json: object, status: status}.merge(opts)

    puts
"++++++++ response: #{response.inspect}"
    render response
 
end

The 'puts' displays:

++++++++ response: {:json=>#<Double :model>, :status=>:ok}

 
But the error says is related to Module::DelegationError:

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'

 

belgoros

unread,
Feb 27, 2019, 10:47:39 AM2/27/19
to rspec
Finally, the version that works:

#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
end



It will fail if I try to call Response module methods that call render inside. Can't figure out how to get around if it.

 

belgoros

unread,
Feb 27, 2019, 11:26:41 AM2/27/19
to rspec
I found a solution to make it work: I override render method in FakeController as follows:

require '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
end

And to test one of Response module methods, here is how I proceed:

context '#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

Reply all
Reply to author
Forward
0 new messages