I'm currently working on a team that requires 100% code coverage and I cannot for the life of me get hit this single line method to get my current coverage up to 100%.
I have a base controller that looks like this which multiple other controller extend from.
module Owners
module Pets
class BaseController < Owners::ApplicationController
private
def current_pet
current_owner.all_pets.find(params[:pet_id])
end
end
end
end
My spec for this controller looks like this.
require 'rails_helper'
Rails.describe Owners::Pets::BaseController, type: :controller do
routes { Owners::Engine.routes }
controller Owners::Pets::BaseController do
def index
current_pet
end
end
let(:user) { double :user, id: 1, owner: nil }
before { sign_in(user) }
before do
allow(request.env['warden']).to receive(:user).and_return user
allow(Owners::User).to receive(:find).with(user.id).and_return user
end
context 'with current owner and pet' do
let(:owner) { create :owner }
let(:pet) { create(:owner_pet, owner: owner) }
describe '#current_pet' do
before do
allow(controller).to receive(:current_pet).and_return pet
routes.append { get 'index' => 'owners/pets/base#index' }
end
it do
get :index
is_expected.to eq pet
end
end
end
end
The spec is failing with the error "No route matches {:action=>"index", :controller=>"owners/pets/base"}" Routes.append should add the proper route, correct?
Update: By moving my routes { Owners::Engine.routes } line above the anonymous controller it no longer throws an error related to routes. However now it is comparing pet with the actual controller class. The output is too long to paste here but it's essentially:
expected: #<Owners::Pet>
got: #<#<Class:0x007fc65c860518>:0x007fc65f83a248>
with a whole bunch of attributes and methods.
This test has no value. You're stubbing the very method that you're testing. Even if the method body of #current_pet raised an exception, the test would still pass.
Generally, it's best to avoid testing private methods directly. You should be able to test this method indirectly via one of the classes which inherits from Owners::Pets::BaseController.
When you use the syntax it { is_expected.to ... } Rspec must infer what "it" is based on the test itself. The subject method can be used to explicitly specify what "it" is; in cases where subject is not present, Rspec will instantiate a new instance of the class which is being tested. In your case that would be the controller itself.
Try setting the subject explicitly inside the context of the #current_pet block.
For example,
context 'with current owner and pet' do
let(:owner) { create :owner }
let(:pet) { create(:owner_pet, owner: owner) }
describe '#current_pet' do
before do
allow(controller).to receive(:current_pet).and_return pet
routes.append { get 'index' => 'owners/pets/base#index' }
end
# set this to whatever you want "is_expected.to" to be
subject { controller.current_pet }
it do
get :index
is_expected.to eq pet
end
end
end
Obligatory Note: I have to agree with other posters that this test is not very useful. Conventional wisdom is to only test public methods (private methods get tested by their usage within a public method).
Related
I want to test a destroy action that I know to be working, but I want a test for it.
It is a destroy action.
The route:
DELETE /journals/:id(.:format) journals#destroy
In the admin/journals.rb registration block have a controller block to override the destroy method.
ActiveAdmin.register Journal do
controller do
def destroy
# do stuff and redirect.
end
end
end
My test
require 'rails_helper'
RSpec.describe JournalsController, type: :controller do
let!(:journal) { Journal.create(title: 'Title1') }
it 'sets the out_of_stock attribute to true' do
expect {
delete :destroy, params: { id: journal.id }
}.to change { journal.reload.out_of_stock }.from(false).to(true)
end
end
For some reason this does not hit the method, nor does it pass.
Result
1) JournalsController destroy journal is in stock and receives destroy sets the out_of_stock attribute to true
Failure/Error:
expect {
delete :destroy, params: { id: journal.id }
}.to change { journal.reload.out_of_stock }.from(false).to(true)
expected `journal.reload.out_of_stock` to have changed from false to true, but did not change
# ./spec/controllers/journals_controller_spec.rb:15:in `block (4 levels) in <top (required)>'
If I override #show in the same manner and call it from a spec with
get :show, params: {id: journal.id }
it does hit the action inside the registration block.
I have tried other text book examples of controller transactions like creating a user, but it just doesn't work.
I do however have an api namespace where tests pass. It only calls get :index though.
Is there some special AA rspec settings that I don't have? Maybe in spec_helper/rails_helper.
How do I make my test hit my action?
Any help is appreciated.
It turns out I hadn't taken some permission checks into account and the test got redirected. Setting up a current_user with the correct role (in my case) fixed the issue. The destroy action was ultimately called.
I have a very simple controller that looks like this.
module Veterinarians
module Dictionaries
class SpecialitiesController < ApplicationController
respond_to :json
skip_before_action :check_current_vet, only: %i( index )
def index
#specialities = Veterinarians::Speciality.all
respond_with(#specialities)
end
end
end
end
I have an rspec controller test that looks like this.
require 'rails_helper'
Rails.describe Veterinarians::Dictionaries::SpecialitiesController, type: :controller do
# Not returning a response body in JSON when testing RSPEC (https://github.com/rails/jbuilder/issues/32)
render_views true
routes { Veterinarians::Engine.routes }
let(:user) { double :user, id: 123 }
before { sign_in(user) }
context '#index' do
let(:speciality) { double :speciality, id: :some_id, value: :some_val }
before { allow(Veterinarians::Speciality).to receive(:all).and_return [speciality] }
subject { get :index, format: :json }
it { is_expected.to have_http_status(:ok) }
it { expect(JSON.parse(subject.body)).to include('id' => 'some_id', 'value' => 'some_val') }
end
end
The second example fails with this error.
expected [{"__expired" => false, "name" => "speciality"}] to include {"id" => "some_id", "value" => "some_val"}
Any hints as to why this would fail and where the hash with "__expired" is coming from?
I have other tests that are using this same method of testing that are successful.
I suspect this is coming from RSpec's internal representation of a double:
https://github.com/rspec/rspec-mocks/blob/master/lib/rspec/mocks/test_double.rb#L10
RSpec's doubles sometimes don't work well alongside Rails. Try instantiating a real Speciality instance, or using something like FactoryGirl to do so.
Rails winds up calling to_json on the double. You haven't stubbed that method on the double, so the to_json method rails adds to Object is called.
This implementation just dumps the instance variables of the object, which in this case is the internal state of the test double.
You could stub to_json on the double, although your spec wouldn't be testing a whole lot at that point.
You should use factories for creating your test data. Two most popular ones are FactoryGirl or FactoryBot.
Example:
FactoryGirl.define do
factory :user do
sequence(:email) { |n| "name#{n}#example.com" }
password 'password'
end
end
The sequence(:email) will create you a different email for each user. More details can be found here.
This is my controller:
class PlansController
def use_new_card
#user = User.find_by_id(new_card_params[:user_id])
if new_stripe_card
...
end
private
def new_card_params
params.permit(:user_id, :stripeToken)
end
def new_stripe_card
StripeService.new({user_id: #user.id, customer_id: #user.stripe_customer_id, source: new_card_params[:stripeToken]}).new_card
end
end
I'm trying to write a controller spec that tests that when the proper parameters are POST to the use_new_card method, then new_stripe_card's StripeService.new gets these parameters appropriately.
My spec looks like this so far:
describe "when proper params are passed" do
before do
#user = User.create(...)
end
it "should allow StripeService.new to receive proper parameters" do
StripeService.should_receive(:new).with({user_id: #user.id, customer_id: #user.stripe_customer_id, source: "test"})
post :use_new_card, user_id: #user.id, stripeToken: "test"
end
end
But with that I'm getting
Failure/Error: post :use_new_card, user_id: #user.id, stripeToken: "test"
NoMethodError:
undefined method `new_card' for nil:NilClass
Totally fair, but I'm not sure how to fix this... I can't just stub new_card because a stubbed method on a nil object will still throw this error (I tried adding a StripeService.any_instance.stub(:new_card).and_return(true) already into the before block)
Stubbed methods return nil by default. Use and_return to specify the value returned by the stub::
StripeService.should_receive(:new).and_return(whatever)
or using the newer syntax
expect(StripeService).to receive(:new).and_return(whatever)
EDIT
Pardon my hand-waving. Your stub must return an object that will act like an instance of StripeService to the extent required for the purposes of the test. So for example:
let(:new_card) { double }
let(:new_stripe_service) { double(new_card: new_card) }
...
expect(StripeService).to receive(:new).and_return(new_stripe_service)
Now when the test refers to new_stripe_service, RSpec will return a test double that has a method stub named #new_card, which itself is a double. You can add whatever additional stubs you need to make the test pass. Alternatively, you can return an actual instance of StripeService for new_stripe_service.
I'm trying to write a rspec test for a mixin class. I have the following.
module one
module two
def method
method_details = super
if method_details.a && method_details.b
something
elsif method_details.b
another thing
else
last thing
end
end
end
end
Now I have mocked the "method" object that will be passed to the class.
But I'm struggling to access the super method.
I did,
let(:dummy_class) { Class.new { include one::two } }
How to pass the mocked method object to this dummy class?
How do I go about testing this? New to ruby, can someone show me a direction with this.
Thanks in advance.
UPDATE:
I tried,
let(:dummy_class) {
Class.new { |d|
include one::two
d.method = method_details
}
}
let (:method_details){
'different attributes'
}
still doesn't work. I get undefined local variable or method method_details for #<Class:0x007fc9a49cee18>
I personally test mixing with the class. Because the mixing (module) itself has no meaning unless its attached to a class/object.
Ex:
module SayName
def say_name
p 'say name'
end
end
class User
include SayName
end
So I believe you should test your module with attached to the relevant class / object.
How ever this is a different perspective on testing mixings
HTH
I think that in your specs, you'll need to explicitly provide a super class definition for when super is called in #method as "you can't mock super and you shouldn't".
I've attempted to spec out all three of your scenarios with the following minor changes:
Changed your example code slightly to become valid Ruby
Changed #method to #the_method so it doesn't conflict with Object#method
Used OpenStruct to represent the object that super returns, because all I know is that it's an object that has methods #a and #b. You can change that out as appropriate for your real specs
Copy and paste the class and specs below into a file and give them a try:
module One
module Two
def the_method
method_details = super
if method_details.a && method_details.b
'something'
elsif method_details.b
'another thing'
else
'last thing'
end
end
end
end
RSpec.describe One::Two do
require 'ostruct'
let(:one_twoable) { Class.new(super_class) { include One::Two }.new }
describe '#the_method' do
let(:the_method) { one_twoable.the_method }
context 'when method_details#a && method_details#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: true, b: true)
end
end
end
it 'is "something"' do
expect(the_method).to eq('something')
end
end
context 'when just method#b' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: true)
end
end
end
it 'is "another thing"' do
expect(the_method).to eq('another thing')
end
end
context 'when neither of the above' do
let(:super_class) do
Class.new do
def the_method
OpenStruct.new(a: false, b: false)
end
end
end
it 'is "last thing"' do
expect(the_method).to eq('last thing')
end
end
end
end
just a simple question for a render json call I'm trying to test. I'm still learning rspec, and have tried everything and can't seem to get this to work. I keep getting an ActionController::RoutingError, even though I defined the route and the call to the api itself works.
In my controller I have the method:
class PlacesController < ApplicationController
def objects
#objects = Place.find(params[:id]).objects.active
render json: #objects.map(&:api)
end
end
with the render json: #objects.map(&:api), I'm calling the api method in the Object model
class Object
def api
{ id: id,
something: something,
something_else: something_else,
etc: etc,
...
}
end
end
My routes file:
get "places/:id/objects" => "places#objects"
my rspec: spec/controllers/places_controller_spec.rb
describe "objects" do
it "GET properties" do
m = FactoryGirl.create :object_name, _id: "1", shape: "square"
get "/places/#{m._id}/objects", {}, { "Accept" => "application/json" }
expect(response.status).to eq 200
body = JSON.parse(response.body)
expect(body["shape"]).to eq "square"
end
end
I keep getting the error
Failure/Error: get "/places/1/objects", {}, { "Accept" => "application/json" }
ActionController::RoutingError:
No route matches {:controller=>"places", :action=>"/places/1/objects"}
Any help would be much appreciated, thanks.
Because you have the spec in the controllers folder RSpec is assuming it is a controller spec.
With controller specs you don't specify the whole path to the route but the actual controller method.
get "/places/#{m._id}/objects", {}
Should be
get :objects, id: m._id
If you don't want this behaviour you can disable it by setting the config infer_spec_type_from_file_location to false. Or you could override the spec type for this file by declaring the type on the describe
describe "objects", type: :request do - change :request to what you want this spec to be.
Although I recommend using the directory structure to dictate what types of specs you are running.