Rails 4 Testing with RSpec and FactoryGirl, nested attributes not saving - ruby-on-rails

Ive been forcing myself to learn Rails4 with BDD and so far its come along quite nicely. However, I have been hammering at this issue for a few hours now and not come up with a reason for why it isnt working. I am currently working on testing Controllers, with only controllers and models built, no forms or anything else. It works via console, so I must simply be using something wrong.
Here is my code for reference:
/admin/pages_controller.rb
class Admin::PagesController < ApplicationController
def update
#page = Page.find_by_id(params[:id])
#page.update_attributes!(message_params)
redirect_to edit_admin_page_path(#page)
end
private
def message_params
params.require(:page).permit(
:url,
:position,
:name,
:tags,
images_attributes:
[:image_file_name, :image_file_size, :image_content_type, :name, :caption, :tags, :owner_id, :owner_type],
block_attributes:
[:id, :body, :owner_id, :owner_type]
)
end
end
models/page.rb
class Page < ActiveRecord::Base
validates :name, presence: true
before_save :validate_url
has_many :images, :as => :owner
has_one :block, :as => :owner
accepts_nested_attributes_for :images, :block
def validate_url
if url.blank?
self.url = self.name.strip.downcase.gsub(" ","-").gsub(%r([^0-9a-z-]), '').gsub("--","-")
end
end
end
pages_controller_spec.rb
describe "POST #update" do
before :each do
#page = FactoryGirl.create(:page)
end
it "makes sure user can upload an image" do
image = FactoryGirl.build(:image, owner_id: #page.id, owner_type: "Page")
post :update, id: #page, page: { :images => [ image ]}
#page.reload
expect(#page.images.first).to eq(image)
end
it "updates values of the attributes accordingly" do
post :update, id: #page, page: { :name => 'foo', :url => 'bar' }
#page.reload
expect(#page.name).to eq('foo')
end
it "updates the values of the block" do
#page = FactoryGirl.create(:page_with_block)
post :update, id: #page, page: { block: { :body => 'foobar' } }
#page.reload
expect(#page.block.body).to eq('foobar')
end
end
spec/factories/blocks.rb
FactoryGirl.define do
factory :block do
body "<html><body><h1>Hello world!</h1><section>This is the content section</section></body></html>"
end
end
spec/factories/images.rb
include ActionDispatch::TestProcess
FactoryGirl.define do
factory :image do
image { fixture_file_upload Rails.root.to_s + '/spec/images/1.jpg', 'image/jpg'}
name { Faker::App.name }
caption { Faker::Lorem.sentence }
tags { Faker::Lorem.words }
end
end
RSpec results:
1) Admin::PagesController POST #update makes sure user can upload an image
Failure/Error: expect(#page.images.first).to eq(image)
expected: #<Image id: nil, image_file_name: "1.jpg", image_file_size: 15078, image_content_type: "image/jpg", name: "Alpha", caption: "Assumenda et exercitationem quo.", tags: ["doloribus", "maiores", "dicta"], owner_id: 37, owner_type: "Page", created_at: nil, updated_at: nil>
got: nil
(compared using ==)
# ./spec/controllers/admin/pages_controller_spec.rb:111:in `block (3 levels) in <top (required)>'
2) Admin::PagesController POST #update updates the values of the block
Failure/Error: expect(#page.block.body).to eq('foobar')
expected: "foobar"
got: "<html><body><h1>Hello world!</h1><section>This is the content section</section></body></html>"
(compared using ==)
# ./spec/controllers/admin/pages_controller_spec.rb:124:in `block (3 levels) in <top (required)>'
Finished in 3.95 seconds (files took 3.71 seconds to load)
19 examples, 2 failures
models/image.rb
class Image < ActiveRecord::Base
validates_presence_of :image_file_name, :image_file_size, :image_content_type
before_save :clean_up
belongs_to :page, foreign_key: "owner_id"
has_attached_file :image
validates_attachment_content_type(:image, :content_type => /^image\/(jpg|jpeg|pjpeg|png|x-png|gif)$/)
validates :image, :attachment_presence => true
def clean_up
if name.blank?
self.name = self.image_file_name.strip.downcase.gsub(" ","-").gsub(%r([^0-9a-z-]), '').gsub("--","-")
end
end
end
Any help would be appreciated on this one, as most of the searches ive found about this subject deal with forms or params.require. I am able to update the attributes of the page itself with the second spec, but nothing I do seems to make it into the nested resources. What am I missing here? Thanks in advance!
Edit: updated to include image model

First of all, avoid using fixture file upload with factory_girl http://pivotallabs.com/avoid-using-fixture-file-upload-with-factorygirl-and-paperclip/
You can simple use it like the following:
FactoryGirl.define do
factory :image do
image { File.new(Rails.root.join('spec/images/1.jpg') }
name { Faker::App.name }
caption { Faker::Lorem.sentence }
tags { Faker::Lorem.words }
end
end
It's not a clear answer, it's just a way in the right direction:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new("spec/factories/files/file.csv") }
let!(:page) { FactoryGirl.create(:page) }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page)
end
let(:image) { page.images.first }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images_attributes => { "0" => image_params } }
end.to change { page.reload.images.count }.by(1)
# Now compare the attributes
expect(image.content_type).to eq(image_attrs[:some_type])
# ...
end
it "updates values of the attributes accordingly" do
expect do
post :update, id: page, page: { name: 'foo', url: 'bar' }
end.to change { page.reload.name }.to("foo")
end
context "updates the values of the block" do
let(:page) { FactoryGirl.create(:page_with_block) }
it do
expect do
post :update, id: page, page: { block: { :body => 'foobar' } }
end.to change { page.reload.block.body }.to("foobar")
end
end
end

Changed the spec to this due to comment:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new('spec/images/1.jpg') }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page, image: img)
end
let!(:page) { FactoryGirl.create(:page_with_block) }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images => { "0" => image_attrs } }
end.to change { page.reload.images.count }.by(1)
end
it "updates values of the attributes accordingly" do
post :update, id: page, page: { :name => 'foo', :url => 'bar' }
page.reload
expect(page.name).to eq('foo')
end
it "updates the values of the block" do
expect do
post :update, id: page, page: { block: { :body => 'foobar' } }
end.to change { page.reload.block.body }.to("foobar")
end
end
Problem still occurs:
................F.F
Failures:
1) Admin::PagesController POST #update makes sure user can upload an image
Failure/Error: expect do
expected result to have changed by 1, but was changed by 0
# ./spec/controllers/admin/pages_controller_spec.rb:111:in `block (3 levels) in <top (required)>'
2) Admin::PagesController POST #update updates the values of the block
Failure/Error: expect do
expected result to have changed to "foobar", but did not change
# ./spec/controllers/admin/pages_controller_spec.rb:123:in `block (3 levels) in <top (required)>'
Finished in 1.32 seconds (files took 1.34 seconds to load)
19 examples, 2 failures
Failed examples:
rspec ./spec/controllers/admin/pages_controller_spec.rb:110 # Admin::PagesController POST #update makes sure user can upload an image
rspec ./spec/controllers/admin/pages_controller_spec.rb:122 # Admin::PagesController POST #update updates the values of the block
EDIT:
It turns out I didnt properly allow for image_attributes to work well with paperclip and basicly a lot of minor things were wrong. I was more used to how the setup was with Rails 3, and misinterpreted how things for Rails 4 gems work.
Updated spec:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new(Rails.root.join('spec/images/1.jpg'), 'image/jpg') }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page, image: img)
end
let(:block_attrs) do
FactoryGirl.attributes_for(:block, owner: page, :body => 'foobar')
end
let!(:page) { FactoryGirl.create(:page_with_block) }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images_attributes => { "0" => image_attrs } }
end.to change { page.reload.images.count }.by(1)
end
it "updates values of the attributes accordingly" do
post :update, id: page, page: { :name => 'foo', :url => 'bar' }
page.reload
expect(page.name).to eq('foo')
end
it "updates the values of the block" do
expect do
post :update, id: page, page: { :block_attributes => block_attrs }
end.to change { page.reload.block.body }.to("foobar")
end
end
Thanks to #ole for pointing me in the right direction for testing code!

Related

How to write optimistic locking test cases in Rails

I need to test if my implementation of optimistic locking is correct or not. But I don't know how to test my functionalities. Here is the update action I wrote:
def update
begin
#update_operator = Operator.find(params[:id])
authorize! :update, #update_operator
if #update_operator.update_attributes(operator_params)
render json: #update_operator, except: :badge
else
render json: #update_operator.errors, status: :unprocessable_entity
end
rescue ActiveRecord::StaleObjectError
#update_operator.reload
retry
end
end
And here is the migration I added
class AddLockingColumnsToOperators < ActiveRecord::Migration[5.1]
def up
add_column :operators, :lock_version, :integer, :default => 0, :null => false
end
def down
remove_column :operators, :lock_version
end
end
Can anyone tell me how to test the update action above with rspec?
Update: Here is the attempt I tried, but it didn't work
let!(:operator1) { FactoryBot.create(:operator, :site => site) }
let!(:attributes) {
{
id: operator1.id,
first_name: "test1",
last_name: "test2",
employee_number: "tesnt12345",
badge: "test215235",
suspended: true,
site_id: site.id
}
}
let!(:stale_attributes) {
{
id: operator1.id,
first_name: "test_fake",
last_name: "test_fake",
employee_number: "tesnt12345",
badge: "test215235",
suspended: true,
site_id: site.id
}
}
it("cause StaleObjectError when updating same operator at the same time") do
patch :update, params: { :id => operator1.id, :operator => attributes }
patch :update, params: { :id => operator1.id, :operator => stale_attributes }
expect(response).to raise_error(ActiveRecord::StaleObjectError)
end
You want a test case where optimistic locking fails.
First get your edit form.
Then update the record from an independent update.
Then submit the edit form.
In a functional test it might look like this:
visit "/operators/#{operator.id}/edit"
indep_operator = Operator.find(operator.id)
indep_operator.update!( ... some attributes ...)
fill_in "Name", :with => "New Value"
click_button "Update Operator"

Issue with apipie gem and rspec in rails 4

i'm writing the code to get my Rspec tests to pass on my api. I'm using the apipie gem to generate documentation and it seems that my tests are failing because thy are expecting a number and it's funny because this is exactly what I want to test.
The page fails when the :bpm parameter is not a number. is there any way of going around this ?
context "when is not created" do
before(:each) do
user = FactoryGirl.create :user
#invalid_lesson_attributes = { title: "California Dreamin",
bpm: "Hello"
}
request.headers['Authorization'] = user.auth_token
post :create, { user_id: user.id, lesson: #invalid_lesson_attributes }
end
it "renders an errors json" do
lesson_response = json_response
expect(lesson_response).to have_key(:errors)
end
it "renders the json errors on why the user could not be created" do
lesson_response = json_response
expect(lesson_response[:errors][:bpm]).to include "is not a number"
end
it { should respond_with 422 }
end
end
Update spec:
context "when is not updated" do
before(:each) do
patch :update, { user_id: #user.id, id: #lesson.id,
lesson: { bpm: "ten" }, format: :json }
end
it "renders an errors json" do
lesson_response = json_response
expect(lesson_response).to have_key(:errors)
end
it "renders the json errors on why the user could not be updated" do
lesson_response = json_response
expect(lesson_response[:errors][:bpm]).to include "is not a number"
end
it { should respond_with 422 }
end
in my users_controller:
api :POST, '/teachers/:user_id/lessons/', "Create lesson"
param :lesson, Hash, desc: 'Lesson information', :required => true do
param :title, String, desc: 'Title of the lesson', :required => true
param :bpm, :number, desc: 'tempo of the lesson (beats per second)', :required => true
end
error :code => 422, :desc => "Unprocessable Entity"
my error when I run my rspec tests :
Apipie::ParamInvalid: Invalid parameter 'bpm' value "Hello": Must be a number.
Adds format json to post request
post :create, { user_id: user.id, lesson: #invalid_lesson_attributes, format: :json }
That worked for me.

Rspec has_one relation error when updating

I have User (which is I used Devise) and Profile model where User has_one Profile as their relationship. I got an error when run the rspec test. Below is my spec to handle when user is updating his/her profile.
spec/controller/profiles_controller_spec.rb
RSpec.describe ProfilesController, type: :controller do
let(:profile) { FactoryGirl.create(:profile) }
let (:valid_attributes) { FactoryGirl.attributes_for(:profile) }
let (:invalid_attributes) { FactoryGirl.attributes_for(:profile).merge({fname: nil}) }
let(:user) { FactoryGirl.create(:user) }
let(:valid_session) { sign_in(user) }
describe "PUT #update" do
before { valid_session }
context "with valid attributes" do
it "saves valid profile" do
expect do
put :update, { id: profile.id, profile: { fname: "Cena" } }
end.to change{ profile.reload.fname }.to("Cena")
end
end
spec/factories/profiles.rb
FactoryGirl.define do
factory :profile do
user
fname "John"
lname "Doe"
avatar "my_avatar"
end
end
app/controller/profiles_controller.rb
class ProfilesController < ApplicationController
private
def user_params
params.require(:user).permit(:id, :login, :email,
profile_attributes: [
:id, :user_id, :fname, :lname, :avatar, :avatar_cache
])
end
end
And here is the error when run rspec spec/controllers/accounts_controller_spec.rb
Failures:
1) AccountsController PUT #update with valid attributes saves valid profile
Failure/Error: put :update, {id: profile.id, user_id: user.id, profile: { fname: "Cena" }}
ActionController::ParameterMissing:
param is missing or the value is empty: user
let(:user) { FactoryGirl.create(:user) }
let(:profile) { FactoryGirl.create(:profile, user_id: user.id ) }
describe "PUT #update" do
before { valid_session }
context "with valid attributes" do
it "saves valid profile" do
expect do
put :update, id: user.id, user: { profile_attributes: { user_id: user.id, fname: "Cena" } }
end.to change{ profile.reload.fname }.to("Cena")
end
end
end
The profiles_controller.rb code you posted is missing the update action (and also the class name is AccountController), but I guess you are doing something like user.update(user_params).
If that's the case, as the error says, the params passed from the controller spec does not have :user key, and that is causing the error.
Assuming from the #user_params method and the error, the params passed to the post in controller spec needs to look like the following:
{
user: {
id: xxx, ...,
profile_attributes: {
id: xxx,
fname: xxx, ...
}
}
}

RSPEC test NAME ERROR - Undefined Local variable or method

I'm a beginner in ruby on rails and programming in general.
I have an assignment where I have to test my rspec model Vote, and as per instructions the test should pass.
When I run rspec spec/models/vote_spec.rb on the console, I receive the following error:
.F
Failures:
1) Vote after_save calls `Post#update_rank` after save
Failure/Error: post = associated_post
NameError:
undefined local variable or method `associated_post' for #<RSpec::ExampleGroups::Vote::AfterSave:0x007f9416c791e0>
# ./spec/models/vote_spec.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.28533 seconds (files took 2.55 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/vote_spec.rb:21 # Vote after_save calls `Post#update_rank` after save
Here is my vote_spec code:
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
up_vote = Vote.new(value: 1)
expect(up_vote.valid?).to eq(true)
down_vote = Vote.new(value: -1)
expect(down_vote.valid?).to eq(true)
invalid_vote = Vote.new(value: 2)
expect(invalid_vote.valid?).to eq(false)
end
end
end
describe 'after_save' do
it "calls `Post#update_rank` after save" do
post = associated_post
vote = Vote.new(value: 1, post: post)
expect(post).to receive(:update_rank)
vote.save
end
end
end
And here is my post_spec code:
require 'rails_helper'
describe Post do
describe "vote method" do
before do
user = User.create
topic = Topic.create
#post = associated_post
3.times { #post.votes.create(value: 1) }
2.times { #post.votes.create(value: -1) }
end
describe '#up_votes' do
it "counts the number of votes with value = 1" do
expect( #post.up_votes ).to eq(3)
end
end
describe '#down_votes' do
it "counts the number of votes with value = -1" do
expect( #post.down_votes ).to eq(2)
end
end
describe '#points' do
it "returns the sum of all down and up votes" do
expect( #post.points).to eq(1) # 3 - 2
end
end
end
describe '#create_vote' do
it "generates an up-vote when explicitly called" do
post = associated_post
expect(post.up_votes ).to eq(0)
post.create_vote
expect( post.up_votes).to eq(1)
end
end
end
def associated_post(options = {})
post_options = {
title: 'Post title',
body: 'Post bodies must be pretty long.',
topic: Topic.create(name: 'Topic name',description: 'the description of a topic must be long'),
user: authenticated_user
}.merge(options)
Post.create(post_options)
end
def authenticated_user(options = {})
user_options = { email: "email#{rand}#fake.com", password: 'password'}.merge(options)
user = User.new( user_options)
user.skip_confirmation!
user.save
user
end
I'm not sure if providing the Post and Vote models code is necessary.
Here is my Post model:
class Post < ActiveRecord::Base
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
belongs_to :user
belongs_to :topic
default_scope { order('rank DESC')}
validates :title, length: { minimum: 5 }, presence: true
validates :body, length: { minimum: 20 }, presence: true
validates :user, presence: true
validates :topic, presence: true
def up_votes
votes.where(value: 1).count
end
def down_votes
votes.where(value: -1).count
end
def points
votes.sum(:value)
end
def update_rank
age_in_days = ( created_at - Time.new(1970,1,1)) / (60 * 60 * 24)
new_rank = points + age_in_days
update_attribute(:rank, new_rank)
end
def create_vote
user.votes.create(value: 1, post: self)
# user.votes.create(value: 1, post: self)
# self.user.votes.create(value: 1, post: self)
# votes.create(value: 1, user: user)
# self.votes.create(value: 1, user: user)
# vote = Vote.create(value: 1, user: user, post: self)
# self.votes << vote
# save
end
end
and the Vote model:
class Vote < ActiveRecord::Base
belongs_to :post
belongs_to :user
validates :value, inclusion: { in: [-1, 1], message: "%{value} is not a valid vote."}
after_save :update_post
def update_post
post.update_rank
end
end
It seems like in the spec vote model, the method assosicated_post can't be retrieved from the post spec model?
You're absolutely right - because you defined the associated post method inside of post_spec.rb, it can't be called from inside vote_spec.rb.
You have a couple options: you can copy your associated post method and put it inside vote_spec.rb, or you can create a spec helper file where you define associated_post once and include it in both vote_spec.rb and post_spec.rb. Hope that helps!

RSpec controller test: How do I check #organization.destroy is being called?

I thought it might be like:
let(:organization) { mock_model(Organization).as_null_object }
before(:each) do
Organization.stub(:find).and_return(organization)
end
it "calls the destroy action on #organization" do
assigns[:organization].should_receive("destroy")
post :destroy, :id => organization.id
end
..but I get a "can't modify frozen object" error.
Here's how I would write that spec:
describe 'Oragnization#destroy' do
let(:organization) { mock_model(Organization, :id => 1, :destroy => true) }
subject { post :destroy, :id => organization.id }
it { should be_successful }
end

Resources