Is this caused by attr_accessible? - ruby-on-rails

I just lately update my model with attr_accessible fields and suddenly some tests would not work, as i would expect. However, i have a spec like:
context "when user buys a game item" do
let(:inventory) {#user.inventory << Factory(:inventory)}
it "should present an error if the id ..." do
GameItem.stub(:find_by_id).and_return(Factory(:game_item))
#user.inventory.should == 1 # TEST
post :buy, :id => (game_item.id + 1)
flash[:error].should == I18n.t('error.invalid_post')
response.should redirect_to melee_url('Weapon')
end
end
The line #user.inventory.should == 1 is just a check that i made now. The inventory is nil for some reason. Does this happen because of the << operation? I would guess that this is most probable, due to the inventory_id attribute of the User model.
I have to say that attr_accessible generally seems like a hack to me and i kinda don't like it, though i can see why it should be used. Do you think this is the case? If so, how can i stay clear of that check?

let is lazy; it won't call the block unless the variable you're defining is used, and I don't see you accessing inventory anywhere. You access #user.inventory, but that's not the same thing.
Either lose the let definition and just put it in your it block, or make sure you call it first before you make sure it did what it was supposed to.

Related

Ruby on Rails RSpec test for model to only allow one record in database

I am trying to build an RSpec test spec for my model: Logo that will ensure that only a singular record can be saved to the database. When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
How can I make this work using RSpec and FactoryBot?
Here is the code I have tried, unsuccessfully:
# app/models/logo.rb
class Logo < ApplicationRecord
before_create :only_one_row
private
def only_one_row
raise "You can only have one logo file for this website application" if Logo.count > 0
end
end
# spec/factories/logos.rb
FactoryBot.define do
factory :logo do
image { File.open(File.join(Rails.root, 'spec', 'fixtures', 'example_image.jpg')) }
end
end
# spec/logo_spec.rb
require 'rails_helper'
RSpec.describe Logo, type: :model do
it 'can be created' do
example_logo = FactoryBot.create(:logo)
expect(example_logo).to be_valid
end
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
# This is where the trouble lies...
# If I go with .create method I error with the raised error defined in my model file...
example_logo_two = FactoryBot.create(:logo)
# ... if I go with the .build method I receive an error as the .build method succeeds
# example_logo_two = FactoryBot.build(:logo)
expect(example_logo_two).to_not be_valid
end
end
Your validation here is implemented as a hook, not a validation, which is why the be_valid call will never fail. I want to note, there's no real issue here from a logical perspective -- a hard exception as a sanity check seems acceptable in this situation, since it shouldn't be something the app is trying to do. You could even re-write your test to test for it explicitly:
it 'can not have more than one record' do
# Ensure there are no logo records in the database before this test is run.
Logo.destroy_all
example_logo_one = FactoryBot.create(:logo)
expect { FactoryBot.create(:logo) }.to raise_error(RuntimeError)
end
But, if there's a possibility the app might try it and you want a better user experience, you can build this as a validation. The tricky part there is that the validation looks different for an unsaved Logo (we need to make sure there are no other saved Logos, period) versus an existing one (we just need to validate that we're the only one). We can make it one single check just by making sure that there are no Logos out there that aren't this one:
class Logo < ApplicationRecord
validate do |logo|
if Logo.first && Logo.first != logo
logo.errors.add(:base, "You can only have one logo file for this website application")
end
end
end
This validation will allow the first logo to save, but should immediately know that the second logo is invalid, passing your original spec.
When I utilize the .build method for the second call to build a Logo, my test fails because FactoryBot is able to build out a Logo.
That is correct, build does not save the object.
However, if I use the .create method for the second Logo entry in FactoryBot I receive an error for the test because my model raises an error, as instructed, based upon my model's method for the :only_one_row method.
Catch the exception with an expect block and the raise_error matcher.
context 'with one Logo already saved' do
let!(:logo) { create(:logo) }
it 'will not allow another' do
expect {
create(:logo)
}.to raise_error("You can only have one logo file for this website application")
end
end
Note this must hard code the exception message into the test. If the message changes, the test will fail. You could test for RuntimeError, but any RuntimeError would pass the test.
To avoid this, create a subclass of RuntimeError, raise that, and test for that specific exception.
class Logo < ApplicationRecord
...
def only_one_row
raise OnlyOneError if Logo.count > 0
end
class OnlyOneError < RuntimeError
MESSAGE = "You can only have one logo file for this website application".freeze
def initialize(msg = MESSAGE)
super
end
end
end
Then you can test for that exception.
expect {
create(:logo)
}.to raise_error(Logo::OnlyOneError)
Note that Logo.destroy_all should be unnecessary if you have your tests and test database set up correct. Each test example should start with a clean, empty database.
Two things here:
If your whole application only ever allows a single logo at all (and not, say, a single logo per company, per user or whatever), then I don't think there's a reason to put it in the database. Instead, simply put it in the filesystem and be done with it.
If there is a good reason to have it in the database despite my previous comment and you really want to make sure that there's only ever one logo, I would very much recommend to set this constraint on a database level. The two ways that come to mind is to revoke INSERT privileges for the relevant table or to define a trigger that prevents INSERT queries if the table already has a record.
This approach is critical because it's easily forgotten that 1) validations can be purposefully or accidentally circumvented (save(validate: false), update_column etc.) and 2) the database can be accessed by clients other than your app (such as another app, the database's own console tool etc.). If you want to ensure data integrity, you have to do such elemental things on a database level.

Using Rspec should_receive to test that a controller calls a method on an object correctly

OK first, I should say while I've read a lot about should_receive, I'm still not entirely sure I'm understanding the concept behind it, so what I'm doing could potentially be completely not possible.
I have the following:
class PlansController
def destroy
plan = plan.find_by_id(params[:id])
if plan.cancel_stripe_subscription(params[:reason])
flash[:success] = "success"
redirect_to root_path
else
#error handling
end
end
end
class Plan
def cancel_stripe_subscription(reason)
self.status = "canceled"
self.cancellation_reason = reason
if self.save
return true
else
return false
end
end
In my controller spec, I am thinking it makes sense to do a test that the cancel_stripe_subscription method is called successfully (using 1should_receive1), with the right arguments and everything, and another test that the output of the destroy action is correct.
In other words, I thought to write the following controller spec:
describe PlansController, "Destroy Action" do
before do
#plan = Plan.create(...)
end
it "should have called destroy action" do
delete :destroy,
plan: {
id: #plan.id,
reason: "something"
}
assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
end
it "should have called destroy action" do
delete :destroy,
plan: {
id: #plan.id,
reason: "something"
}
assigns(:plan).status.should == "canceled"
assigns(:plan).cancellation_reason.should == "something"
end
end
The second test passes, but the first throws
Failure/Error: assigns(:plan).should_receive(:cancel_stripe_subscription)
(#<Plan:0x007fe282931310>).cancel_stripe_subscription(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
So I really have two questions:
Just to confirm, am I using should_receive correctly? Should I even be testing for this? Or is the second test generally accepted as enough?
If I should be testing for this, what's the right way of using should_receive? (Note, have not had luck with expect(#plan).to have_received(:cancel_stripe_subscription) either)
I think the confusion here is in part due to combining two different styles of testing, mockist (your first test) and classicist (your second test). It's fine to use one or the other, based on your preferred testing style, but using both together to test the same piece of code is somewhat redundant.
A should_receive expectation has to be set before you call the method under test; you're setting it afterwards. Since you need to set it before, you then have to make sure the object you've set up the expectation on ends up being operated on in the action. The normal way would be to stub out find_by_id on Plan, like this:
it "should have called destroy action" do
Plan.stub(:find_by_id).and_return(#plan)
assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
delete :destroy, plan: { id: #plan.id, reason: "something" }
end
(I am assuming that you meant to write plan = Plan.find_by_id(params[:id]) in the first line of your destroy action.)
As to whether you should be testing it this way, I'd say that your second test does a good enough job of verifying the outcome that you want, and you don't really need to go to all the trouble.

Very simple Rspec rails test is not passing, where it should

I'm having some problems creating an rspec test to my rails application.
Let say that I have a model called MyModel, with the following function, that obtains the instances of all the MyModels that have an empty text
self.searchEmtpy:
self.where(:text => nil)
end
I've defined the following test, that checks that, new MyModels with empty text, should be returned by the previous function. I use FactoryGirl for the model creation.
describe "get empty models" do
before do
#previousModels=MyModel.searchEmtpy
#newModel=Factory(:myModel, :text => nil)
end
it "new empty models should appear" do
currentModels=MyModel.searchEmtpy
(previousModels << #newModel).should eq(currentModels)
end
end
The test is quite simple, but its not working. I don't know why, but, for what I understand from the output, it seams that, on the "should" line, previousModels already contains the newModel on it, so the test fails (it contains #newModel 2 times.
I'm missing something obvious? Aren't the instructions inside "it" called in order?
To clarify, the following test does not fail, where it should:
describe "get empty models" do
before do
#previousModels=MyModel.searchEmtpy
#newModel=Factory(:myModel, :text => nil)
end
it "new empty models should appear" do
currentModels=MyModel.searchEmtpy
(previousModels).should eq(currentModels)
end
end
self.where(:text => nil)
Is an ActiveRecord::Relation - the query doesn't actually fire until you try to do something to it (like iterate over it, append to it etc.)
In this case that happens on the same line as your call to should, ie after the factory has created the instance.
One way to fix this would be to force the evaluation of the relation in your before block, for example call .all on it.

Rspec actions that change the DB

I'm a bit confused with the behavior of rpsec tests involving controller methods that affect the DB. I have seen many examples of rspec tests that involve POST's and DELETE's where people check to see that an object was created or deleted. In most of these tests people are able to just check that the count of the model in the DB has increased or descreased with a tests such as:
delete :clear_photos, :id => #album.id
#album.photos.size.should == 0
or with lambdas:
lambda {delete :destroy, :id => #photo.id}.should change(#album.photos, :size).by(-1)
The syntax isn't perfect in the last example but my point is that in my experience, I have needed to call reload on the object in order for any of these tests to pass, but for some reason, other are able to make them work without explicitly calling reload. Something about calling reload every time I am testing a db create/destroy action seems fishy to me.
Can anyone help me understand what's going on? Thanks!
ACTUAL CODE UPDATE
it "should clear all photos for an album" do
#album = Factory(:album, :photos => [Factory(:photo), Factory(:photo)])
delete :clear, :album_id => #album.id
#album.photo_count.should == 0
end
I get this response:
'PhotosController#clear should clear all photos for an album' FAILED
expected: 0,
got: 2 (using ==)
./spec/controllers/photos_controller_spec.rb:17:
If I reload the #album before calling photo_count though, it works.
I would like to point out that testing a model state in a controller spec is not a very good practice, because it violates the isolation of a unit test. You should instead test if a controller response is appropriate for the current scenario.

How do you use errors.add_to_base outside of the validates_ or validate_ model methods?

I have a Course class that has many WeightingScales and I am trying to get the WeightingScales validation to propagate through the system. The short of the problem is that the following code works except for the fact that the errors.add_to_base() call doesn't do anything (that I can see). The Course object saves just fine and the WeightingScale objects fail to save, but I don't ever see the error in the controller.
def weight_attributes=(weight_attributes)
weighting_scales.each do |scale|
scale.weight = weight_attributes.fetch(scale.id.to_s).fetch("weight")
unless scale.save
errors.add_to_base("The file is not in CSV format")
end
end
end
My question is similar to this 1: How can you add errors to a Model without being in a "validates" method?
link text
If you want the save to fail, you'll need to use a validate method. If not, you'll have to use callbacks like before_save or before_create to check that errors.invalid? is false before you allow the save to go through. Personally, i'll just use validate. Hope it helps =)
I had a similar problem, I wanted to validate a parameter that never needed to be saved to the model (just a confirmation flag).
If I did:
#user.errors.add_to_base('You need to confirm') unless params[:confirmed]
if #user.save
# yay
else
# show errors
end
it didn't work. I didn't delve into the source but from playing around in the console it looked like calling #user.save or #user.valid? cleared #user.errors before running the validations.
My workaround was to do something like:
if #user.valid? && params[:confirmed]
#user.save
# redirect to... yay!
elsif !params[:confirmed]
#user.errors.add_to_base('You need to confirm')
end
# show errors
Errors added to base after validations have been run stick around and display correctly in the view.
But this is a little different to your situation as it looks like you want to add errors in a setter not a controller action. You might want to look into before_validation or after_validation instead.

Resources